UNP第五章读书笔记---服务器端/客户端建立连接后可能遇到的各种情况及具体分析...

一、分析所使用的服务器端与客户端demo

1.1 服务器端

void str_echo(int);

int main(int argc, char **argv){
    int listenfd, connfd;
    pid_t childpid;
    socklen_t clilen;
    struct sockaddr_in cliaddr, servaddr;

    //创建监听套接字
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);

    //对套接字地址结构进行清零
    bzero(&servaddr, sizeof(servaddr));

    //填充套接字地址结构(协议族、IP地址与端口号)
    servaddr.sin_family = AF_INET;
    //通配IP地址意味着IP地址由内核来选择
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    //将监听套接字与相应IP地址和端口(特别指定或内核临时分配)进行绑定。套接字地址结构强制转换为通用套接字地址结构
    Bind(listenfd, (SA *) &servaddr, &clilen);

    //将套接字从主动套接字转换成被动套接字,通过监听套接字对相应端口进行监听,看是否有客户端请求连接,并将请求连接的客户端放入未完成连接与已完成连接的两个队列中(由内核完成)
    Listen(listenfd, LISTENQ);

    for(;;)
    {
        clilen = sizeof(cliaddr);
    
        //从已完成队列队列头返回下一个已完成连接,创建相应的已连接套接字
        connfd = Accept(listenfd, (SA *)&cliaddr, &clilen);
    
        //采用TCP并发服务器模式,对每个客户端建立一个新进程进行处理
        if((childpid = Fork()) == 0)
    {
        //子进程进入这个条件判断语句内部,父进程则跳过这一部分直接到Close(connfd)语句处
        //子进程用来处理客户请求,只需要已连接套接字描述符即可,由父进程继续进行请求监听,所以子进程关闭监听套接字避免资源浪费
        Close(listenfd);
        //子进程对客户请求的回应
        str_echo(connfd);
        exit(0);
    }
    
    //既然由子进程来处理客户请求,父进程只需继续进行监听,所以父进程关闭已连接套接字
    Close(connfd);
    }

}    

void str_echo(int sockfd)
{
    ssize_t n;
    char buf[MAXLINE];

    while((n = read(sockfd, buf, MAXLINE)) > 0)
   {
        //首字符大写的都是包裹函数,包裹了对应小写的系统函数与一些错误处理语句,提高代码可读性
        Writen(sockfd, buf, n);
    
        if(n < 0 && errno == EINTR)
        {
        continue;
        }
        else if(n < 0)
        {
        //错误输出,用printf代替也可
            err_sys("str_echo:read error");
        }
    }
}


1.2 客户端

void str_cli(FILE *fp, int sockfd)
{
    char sendline[MAXLINE], recvline[MAXLINE];
    while(Fgets(sendline, MAXLINE, fp) != NULL)
    {
        //通过套接字向对方(服务器端)写
        Writen(sockfd, sendline, strlen(sendline));
    
        //通过套接字向对方(服务器端)读
        if(Readline(sockfd, recvline, MAXLINE) == 0)
        {
            err_quit("str_cli:server terminated prematurely");
        }
    
        Fputs(recvline, stdout);
    }
}


int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    if(argc != 2)
    {
        err_quit("Usage:tcpcli <IPaddress>");
    }

    //创建网际套接字,获得套接字描述符
    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    //对套接字地址结构进行清零
    bzero(&servaddr, sizeof(servaddr));

    //填充套接字地址结构(协议族、IP地址与端口号)
    //客户端套接字绑定的IP地址与端口号要与服务器端相同,不然无法连接
    //服务器端的IP地址由命令参数中得到
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(9999);
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    //向服务器发出请求,请求连接
    Connect(sockfd, (SA *)& servaddr, sizeof(servaddr));

    //linux一切皆文件,标准输入(0),标准输出(1)与错误输出(3)也不例外
    str_cli(stdin, sockfd);

    exit(0);
}


二、大致流程图

大致流程图

三、7种可能的情况及分析

3.1 Tcp状态转移图

为了更好的了解,先祭出Tcp状态转移图:
Tcp状态转移图

3.2 Tcp分组交换图

为了便于后面具体分析这些情况时能有个对照,特贴出Tcp分组交换图:
Tcp分组交换图

3.3 具体情况分析

  • 双方均正常启动
    • 首先启动服务器端,依次调用socket、bind、listen和accept,并阻塞与accpt调用。服务器端套接字状态为SYN_RCVD。
    • 启动客户端,依次调用socket与connect,由connet引起TCP三次握手过程建立起连接。三次握手结束后,客户端connect返回(客户接收到三次握手的第二个分节时,见Tcp分组交换图),服务器端accpet也返回(服务器端接收到第三个分节时,见Tcp分组交换图),连接正式建立起来,服务器段得到了已连接队列队首连接。套接字状态皆为ESTABLISHED。
    • 进入套接字读写过程。
      • 客户调用str_cli函数,阻塞与fgets调用,等待客户输入。
      • 服务器中accept返回后,fork()进入子进程,子进程调用str_echo()-->readline()-->read(),等待客户通过套接字传来内容。
      • 服务器父进程将再次阻塞于accept,等待下一个客户连接。
  • 双方均正常终止(如何处理/避免僵死子进程)
    • 客户端键入EOF字符,fgets()返回一个空指针,导致str_cli()-->main()-->exit()。客服端进入进程终止处理过程,由内核关闭已经打开的套接字。由于套接字的关闭,客户端向服务器端发送FIN,服务器回复以ACK,TCP连接终止序列完成了前半部分(见分组序列图)。此时服务器端处于CLOSE_WAIT状态,客户端处于FIN_WAIT_2状态(等待服务器端发送FIN n后再回复Ack n+1完成TCP终止序列)。
    • 服务器端接收到FIN时,阻塞于readline的子进程由于readline返回0,导致str_echo()-->main()-->exit(),服务器子进程被终止,子进程中所有打开的描述符被关闭。在关闭已连接描述符时会导致服务器端向客户端发送FIN n,客户端回复ACK n+1后,TCP连接序列发送完成,连接正式终止,客户进入TIME_WAIT状态。
    • 服务器子进程被终止,除了关闭打开的所有的描述符外,会向父进程发送一个SIGCHLD信号,若父进程对此信号未捕获且未处理,则将导致子进程成为僵死进程,导致系统资源的浪费。我们可以配合sigaction函数和waitpid函数,捕获SIGCHLD信号并进行相应处理。
  • 在服务器端accpet返回之前客户段终止连接
    • TCP三路次握手已完成,连接已建立。客户此时向服务器端发送一个RST分节(对于服务器端来说,RST分节在连接已进入已连接队列,等待服务器端通过调用accept来进一步处理连接时收到)。
    • 不同实现对这种情况的处理也不尽相同。
      • (源自Berkeley的实现)在内核中处理中止的连接。(RST-->tcp_close()-->in_pcbdetach()-->sofree(),其中in_pcbdetch()删除相应的协议控制块,sofree()删除已连接队列中的相应队列)。
      • (源自SVR4的实现)通过acccept的返回值返回一个ECONNABORTED错误给服务器进程。
  • 在连接已建立后,服务器端进程终止
    • 服务器进程中止,子进程所有打开的描述符将被关闭,服务器端向客户端发送FIN,客户端回复ACK,TCP终止序列前半部分完成。SIGCHLD信号由子进程发送给父进程并得到正确处理,以避免僵死子进程产生。
    • 客户端进程此时阻塞在fgets()调用上,若此时通过标准输入键入数据并发送给服务器端(由于TCP连接并未完全关闭)。服务器端接收到数据,由于相应描述符已经全部关闭,所以发送RST分节给客户端。客户端并不能收到这个RST分节:在调用writen()后调用readline(),由于服务器端之前发送的FIN,导致readline()返回0(虽然0表示EOF,但此时实际上并未收到EOF(或FIN导致了EOF的发送?)),导致输出出错信息"server terminated prematurely",客户端相关描述符被关闭。
      • 这种情况发生的原因在于,当FIN到达套接字时,客户正阻塞在fgets上,即客户同时在在应对套接字与用户输入这两个描述符。为此引入了select()和poll(),可以使客户端在服务器端子进程终止的第一时间获知其终止状态。
      • 在相似的情形下,若客户端发送了不止一次数据给服务器端,那么第一次的数据会引发RST分节的发送。当再次发送数据时,内核会向进程发送SIGPIPE信号,默认行为是终止相应进程。对这个信号无论是捕获并处理或是直接忽略(SIG_IGN)都会引发EPIPE错误。
  • 在连接已建立后,服务器端主机崩溃(或主机不可达)
    • 在连接已建立,服务器端主机崩溃或不可达时,客服端会持续重传数据分节(Berkeley:重传12次约9分钟),试图从服务器端获得ACK分节回复。若没有获得服务器端的ACK分节,客户端会返回ETIMEOUT错误。若某个中间路由判定服务器不可达,则返回EHOSTUNREACH或ENETUNREACH错误。(若客户端希望不通过主动发送数据也能得知主机不可达,可采用SO_KEEPALIVE套接字选项,使用TCP心跳机制)
  • 在建立连接后,服务器端主机崩溃后重启
    • 连接建立后,若服务器端主机崩溃然后重启,崩溃前的连接信息实际已经全部丢失了,此时若服务器接收到客户端发来的数据分节,会响应RST分节。
    • 客户端收到服务器端的RST分节时正阻塞在readline()调用中,此时会返回ECONNRESET。(可通过设置SO_KEEPALIVE套接字选项使用TCP心跳机制)
  • 在建立连接后,服务器段主机关机
    • 服务器主机若被关机,init进程会向所有进程发送SIGTERM信号(可被捕获),等待一端时间后(5s-20s),init进程给仍在运行的进程发送SIGKILL信号(不可捕获)。
  • (此外关于传输的数据,其格式是否合适将影响数据能否正确传送接受:若双方拥有相同的字符集,则字符串的传输是最保险的(无论文本数据或是数值数据);若使用二进制格式进行数据传输,则需要考虑位数、大小端等问题)

注脚

这些读书笔记均为平时读书时随手记录下来的,之前一直分散在各处,特此将它们集中在一起,便于今后复习用。

转载于:https://www.cnblogs.com/ChyauAng/p/9705265.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值