Socket编程实践(9) --TCP服务器常见问题(4)

TCP/IP协议的11种状态

说明:

1.如下图(客户端与服务器都在本机:双方(server的子进程,与client)链接已经建立(ESTABLISHED),等待通信)

2.最先调用close的一端,后面会进入TIME_WAIT的状态(下图,server端首先关闭)

3.TIME_WAIT 时间是2MSL(报文的最长存活周期的2倍)

      原因:(ACK y+1)如果发送失败可以重发。

  服务器端处于closed状态,不等于客户端也处于closed状态。。

4.TCP/IP协议的第1种状态:图上只包含10种状态,还有一种CLOSING状态

产生CLOSING状态的原因:

Server端与Client端同时关闭(同时调用close,此时两端同时给对端发送FIN包),将产生closing状态,最后双方都进入TIME_WAIT状态(如下图)。

 
 

SIGPIPE

如果对方socket已关闭,对等方再发写数据,则会产生SIGPIPE信号

往一个已经接收FIN的套接中写是允许的,接收到FIN仅仅代表对方不再发送数据;但是在收到RST段之后,如果还继续写,调用write就会产生SIGPIPE信号,对于这个信号的处理我们通常忽略即可。

signal(SIGPIPE, SIG_IGN); 

Client端测试代码:(请注意观察27-39,44,53行代码)

//client端完整代码实现及解析
#include "commen.h"

//return a socket that have connected to server.
int mkATCPClient(int serverPort, string serverIPAddr)
{
    //first. create a socket
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if (sockfd == -1)
    {
        err_exit("socket error");
    }

    //second. connect to a server
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(serverPort);
    serverAddr.sin_addr.s_addr = inet_addr(serverIPAddr.c_str());
    if (connect(sockfd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) == -1)
    {
        err_exit("connect error");
    }

    return sockfd;
}

//捕获SIGPIPE信号的测试代码
void onSignalCatch(int signalNumber)
{
    switch(signalNumber)
    {
    case SIGPIPE:
        cout << "receive SIGPIPE = " << SIGPIPE << endl;
         _exit(0);
    default:
        cout << "UnKnow signal" << endl;
        break;
    }
}

int main()
{
    //安装捕获SIGPIPE信号的处理服务
    signal(SIGPIPE,onSignalCatch);

    int serverSocket = mkATCPClient(8002,"127.0.0.1");

    char sendBuf[BUFSIZ];
    //从键盘输入数据
    while (fgets(sendBuf,sizeof(sendBuf),stdin) != NULL)
    {
        //向server发送数据(注意在server端关闭时也要发送数据,注意观察现象)
        if (writen(serverSocket,sendBuf,strlen(sendBuf)) == -1)
        {
            err_exit("write socket error");
        }
    }
    close(serverSocket);

    return 0;
}

说明:在server端关闭(或者是处理该客户端的子进程关闭)后,仍继续往socket中写数据,则会观察到如下现象:

Close VS. Shutdown

Man-Page

SYNOPSIS

       #include <unistd.h>
       int close(int fd);

       #include <sys/socket.h>
       int shutdown(int sockfd, int how);

DESCRIPTION

close()  closes  a  file  descriptor,  so that it no longer refers to any file and may be reused(引用计数减至0).  Any record locks (记录锁[文件锁]see fcntl(2)) held on the file it  was  associated  with,  and owned  by  the  process,  are removed (regardless of the file descriptor that was used to obtain the lock).

If fd is the last file descriptor referring to the underlying open file description  (see open(2)),  the  resources  associated  with  the  open file description are freed; if the descriptor was the last reference to a file which has been removed  using  unlink(2)  the file is deleted.

The  shutdown() call causes all or part of a full-duplex connection on the socket associated with sockfd to be shut down.  If how is SHUT_RD, further receptions will  be  disallowed.   If  how  is  SHUT_WR,  further  transmissions  will  be  disallowed.   If how is SHUT_RDWR, further receptions and transmissions will be disallowed.

说明:

1.close终止了数据传送的两个方向;而shutdown可以有选择的终止某个方向的数据传送或者终止数据传送的两个方向。

2.shutdown可以保证对等方接收到一个EOF字符,而不管其他进程是否已经打开了套接字(shutdown不管套接字中的引用计数!)。而close不能保证,close需要直到套接字引用计数减为0时才发送。也就是说直到所有的进程都关闭了套接字。

思考1

   客户端向服务器发送:FIN(close)   E  D  C  B  A

   问:服务器还能收到数据吗?服务器还可以向客户端回报文吗?

   客户端想在关闭之后,仍然能接收到回射服务器应答。

思考2

   父进程中close;会不会向客户端发送FIN报文段呢?

   文件的引用计数-1,当减少为0,才会发送引用计数。

思考3:

客户端//shutdown(sock, SHUT_WR);只关闭了写;

实践(server端业务处理部分代码,其余代码同前):

        else if (pid == 0)  //子进程,处理业务
        {
            close(serverSockfd);  //子进程关闭监听套接字,因为子进程不负责监听任务

            char recvBuf[BUFSIZ];
            ssize_t readCount = 0;
            while (true)
            {
                memset(recvBuf,0,sizeof(recvBuf));
                if ((readCount = read(peerSockfd,recvBuf,sizeof(recvBuf))) == -1)
                {
                    err_exit("readn error");
                }
                else if (readCount == 0)
                {
                    peerClosePrint("client connect closed");
                }
                if (strncmp(recvBuf,"close",5) == 0)
                {
                    //close(peerSockfd);
                    shutdown(peerSockfd,SHUT_WR);
                }

                //将整体报文回写回客户端
                if (writen(peerSockfd,recvBuf,strlen(recvBuf)) == -1)
                {
                    err_exit("writen error");
                }

                recvBuf[readCount] = 0;
                //写至终端
                fputs(recvBuf,stdout);
            }
        }
        else if (pid > 0)   //父进程
        {
            //close(peerSockfd);
        }

/**注意:

1.父进程中close调用已经被注释,也就是在只有一个子进程的情况下,peerSockfd的引用计数为2

2.在子进程中,如果调用的是shutdown,通信截图如下:

客户端自动关闭(说明客户端能够收到对方发来的FIN报文,说明shutdown确实将peerSockfd给关掉了,它并不理会peerSockfd的引用计数不为1)

3.如果调用的是close函数(谨记:此时peerSockfd的引用计数并未减为0),通信截图如下:

客户端需要显示的终止(说明客户端并未受到FIN报文,说明close函数需要考虑到peerSockfd引用计数不为1的情况)

*/

时间: 2024-10-29 16:37:58

Socket编程实践(9) --TCP服务器常见问题(4)的相关文章

Socket编程实践(6) --TCPNotes服务器

僵尸进程过程 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中加入 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法.解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 假设多个客户端同一时候关闭, 问题描写叙述如以下两幅图所看到的: watermark/2/text/aHR0cDovL2Jsb2cuY

Socket编程实践(6) --TCP服务端注意事项

僵尸进程处理 1)通过忽略SIGCHLD信号,避免僵尸进程 在server端代码中添加 signal(SIGCHLD, SIG_IGN); 2)通过wait/waitpid方法,解决僵尸进程 signal(SIGCHLD,onSignalCatch); void onSignalCatch(int signalNumber) { wait(NULL); } 3) 如果多个客户端同时关闭, 问题描述如下面两幅图所示: /** client端实现的测试代码**/ int main() { int s

Socket编程实践(6) --TCP粘包原因与解决

流协议与粘包 粘包的表现 Host A 发送数据给 Host B; 而Host B 接收数据的方式不确定 粘包产生的原因 说明 TCP 字节流,无边界 对等方,一次读操作,不能保证完全把消息读完 UDP 数据报,有边界 对方接受数据包的个数是不确定的 产生粘包问题的原因分析 1.SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区.接受缓冲区) 2.tcp传送的端 mss大小限制 3.链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割. 4.tcp的流量控制和拥塞控

socket编程:简单TCP服务器/客户端编程

其实对于socket:我们需要理解的是他提供了一种编程概念,利用socket就可以利用系统已经封装好的内部进行通信,我们只需要关注应用层方面的数据控制就OK了. 一. 套接字(socket) socket英文为插座的意思,也就是为用户提供了一个接入某个链路的接口.而在计算机网络中,一个IP地址标识唯一一台主机,而一个端口号标识着主机中唯一一个应用进程,因此"IP+端口号"就可以称之为socket. 两个主机的进程之间要通信,就可以各自建立一个socket,其实可以看做各自提供出来一个&

Socket编程实践(5) --TCP粘包问题与解决

TCP粘包问题 因为TCP协议是基于字节流且无边界的传输协议, 因此非常有可能产生粘包问题, 问题描写叙述例如以下 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvempmMjgwNDQxNTg5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" /> 对于Host A 发送的M1与M2两个各10K的数据块, Host B 接收数据的方式不确定, 有以下

Socket编程实践(1) --TCP/IP简述

ISO的OSI OSI(open system interconnection)开放系统互联模型是由ISO国际标准化组织定义的网络分层模型,共七层, 从下往上为: OSI七层参考模型 物理层(Physical Layer) 物理层定义了所有电子及物理设备的规范,为上层的传输提供了一个物理介质,本层中数据传输的单位为比特(bit/二进制位).属于本层定义的规范有EIA/TIA RS-232.RJ-45等,实际使用中的设备如网卡属于本层. 数据链路层(Data Link Layer) 对物理层收到的

Socket编程实践(16) --TCP/IP各层报文(1)

以太网帧格式 说明1:链路层的数据包,称为以太网帧. 说明2:链路层不识别IP地址[因为IP地址是逻辑地址],链路层识别物理网卡MAC地址[硬件地址]. 说明3:需要根据IP地址找到对方的MAC地址(ARP地址解析协议)[MAC -> IP地址方向地址解析:RARP反向地址解析协议]. 说明4:应用层根据对等方的IP地址进行通讯,在数据封装过程中,链路层需要目的地址的MAC地址从何而来?需要将IP地址转换成MAC地址,也就是地址解析. 以太网首部代码: struct ethernet_hdr {

Socket编程实践(17) --TCP/IP各层报文(2)

UDP数据报 UDP首部代码: struct udp_hdr { unsigned short src_port; unsigned short dest_port; unsigned short len; unsigned short chksum; }; TCP报文段 协议描述 源端口号和目的端口号:源和目的主机的IP地址加上端口号构成一个TCP连接 序号和确认号:序号为该TCP数据包的第一个数据字在所发送的数据流中的偏移量:确认号为希望接收的下一个数据字的序号: 首部长度,以4个字节为单位

Socket编程实践(10) --select的限制与poll的使用

select的限制 用select实现的并发服务器,能达到的并发数一般受两方面限制: 1)一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n(number)来调整或者使用setrlimit函数设置,但一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看 /**示例: getrlimit/setrlimit获取/设置进程打开文件数目**/ int main() { struct rlimit rl;