Linux - socket

什么是socket

在学习套接口以前,先要回顾一下Tcp/Ip四层模型:编程

而在说明什么是Socket以前,须要理解下面这些图:缓存

而实际上:bash

这跟管道是不一样的,管道只能用于本机的进程间通讯。另外socket能用于异构系统间进行通讯:服务器

IPv4套接口地址结构

通常不用网络

为何要有地址家族呢?由于Socket不只仅只能用于Tcp/Ip协议,还能用于其它协议,如:Unix域协议,因此必定得指名是哪一个家族,若是是IPv4协议,则须要指定为AF_INET,若是是AF_INET6,则就是IPv6协议,这个用得不多~并发

16位的无符号整数,也就是两个字节,它能表示的最大的整数为:65535dom

对于IPv4协议,地址是32位的,也就是四个字节,因此该结构体为无符号的32位整数:socket

实际上,也能够经过man帮助来看到其结构:man 7 iptcp

【注意】:日常编程时,只会用到sa_family_t、in_port_t、struct in_addr这三个字段。模块化

通用地址结构

该字段总共有14个字节,实际上跟sockaddr_in最后面三个字段的总和是同样大小的:

因此说,通用的地址结构能够兼容IPv4的结构

为何要有通用地址结构呢? 缘由在于Socket不只仅只能用于Tcp/Ip编程,它还可以用于Unix域协议编程,不一样的协议地址结构形式可能有不同的地方,因此说,这里存在一个统一形式的地址结构,能够用于全部的协议编程。

【提示】:实际编程中,一般是填充sockaddr_in地址结构,最终再强制转换成通用的sockaddr地址结构。

网络字节序

实际上,刚才在查看man帮助时,就出现过这个概念,如:

因此下面来认识一下它:

关于上面的概念,可能有些抽象,下面用图来讲明一下:

为何要引入字节序这样一个概念呢?

这是由于Socket能够用于异构系统之间的通信,不一样的硬件平台,对于一个整数,存放形式是不同的,有的机器是采用的大端字节序,有的则采用的小端,若是传给对等方可能解析出来的数字就会不同了,这时就必须统一字节序,这个字节序就叫作“网络字节序”,因此能够看下面介绍。

这里指的就是本机中的实际字节序,下面能够编写一个小小的程序来验证一下咱们的机器是什么字节序,以下:

编译运行:

字节序转换函数

下面来用代码来讲明一下:

编译运行:

地址转换函数

为何要有地址转换函数呢?由于咱们日常人为认识的地址并非32的数字,咱们比较习惯的地址相似于这样:"192.168.0.100",而咱们编程的时候,更多的是用的32的数字,因此须要引入地址转换函数,以下:

这个函数的功能跟下面这个函数的功能同样,都是将用户识别的地址转换成网络字节序,将存放在inp这个结构体中,第二个参数是一个输出参数。

将用户识别的相似于"192.168.0.100"这样的地址转换成32位的整数,下面用代码来看一下效果:

编译运行:

将32位的网络字节序转换成咱们能识别的ip形式的地址:

编译运行:

套接字类型

对于TCP/IP协议而言,就是tcp协议,若是是其它协议而言那就不必定了。

它提供了一种能力,让咱们跨越传输层,直接对ip层进行数据封装的套接字,经过原始套接字,咱们能够将应用层的数据直接封装成ip层可以认识的协议格式,关于原始套接字的编程以后再来学。

TCP客户/服务器模型

回射客户/服务器

这个例子的效果就是:客户端从命令行获取一行命令,而后发送给服务器端,当服务端接收到这行命令以后,不作任何操做,将其又回送给客户端,而后客户端进行回显,下面则开始一步步来实现这样的效果,来初步感觉下Socket编程:

首先编写服务端:echosrv.c

第一步:建立套接字:

关于第一个参数domain,man帮助中也有说明:

可是,AF_INET等价于PF_INET,这里推荐用后者,由于恰好表明protocol family含义,下面代码以下:

第二步:绑定一个地址到套接字上:

首先准备一下第二个参数,也就是要绑定的地址:

其中绑定地址还有其它两种方式:

另外,其实"servaddr.sin_addr.s_addr = htonl(INADDR_ANY);"这种写法是能够省略掉的,由于它是全0,但这里为了显示说明因此保留。

下面开始进行绑定:

第三步:则开始进行监听:

具体代码以下:

其中SOMAXCONN能够从man帮助中查看到:

它表明了socket的并发最大链接个数。 另外还得注意,套接字有被动套接字和主动套接字之分,当调用listen以后,该socket就变更被动套接字了,须要由主动套接字来发起链接,主动套接字是用connect函数来发起链接的。

第四步:从已完成链接队列中返回第一个链接:

接下来,则进行数据的接收,并将数据回显给客户端: accept函数会返回一个新的套接字,注意:此时的套接字再也不是被动套接字,而变为了主动:

能够经过accept的man手册来得知:

下面,则开始从该套接字中读取客户端发过来的数据:

至此,服务端的代码都已经编写完了,下面则先编译一下:

查看man帮助:

因而在代码中加入头:

再次编译:

仍是出错,那IPPPOTO_TCP是在哪定义的呢? 能够经过如下命令进行查找:

因而乎,加上该头文件后再编译:

用一样的办法来进行查找:

因而加入它:

再次编译:

仍是报错,对于这里面对应的头文件这里就不具体一个个查找了,否则有点充数的嫌疑,将全部头文件加上再次编译:

接下来,开始编写客户端的代码:echocli.c 首先建立一个socket:

第二步开始与服务器进行链接:

【说明】:用connect发起链接的套接字是主动套接字。 链接成功以后,就能够向服务器发送数据了:

另外,服务端在使用资源以后,最后也得关闭掉,因此修改服务端程序以下:

这时,客户端程序也已经编写完成,下面编译运行看一下效果:

也就是第一次客户端输入很长的字符串,而第二次输入很短的字符串时,这时就会输出有问题,照理应该是客户端输入什么,服务端就会回显给客户端,也就是打印两条如出一辙的语句,产生这样的问题缘由是什么呢? 下面来用图来分析一下:

因此,解决该问题的思路就是每次循环时将值初始化一下既可,修改代码以下:

再次编译运行:

关于这个缘由,以后会来解决,先占且不关心,等一会就会正常了,正常运行的效果以下:

就实现了客户端与服务器端的socket通信了,最终的代码以下: echosrv.c【服务端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    char recvbuf[1024];//用来存储客户端发来的数据
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = read(conn, recvbuf, sizeof(recvbuf));//从套接字中读取数据
        fputs(recvbuf, stdout);//打印到屏幕上
        write(conn, recvbuf, ret);//而且将其又回显给客户端,其第三个参数的长度正好是咱们接收到的长度
    }
    close(conn);
    close(listenfd);
    
    return 0;
}
复制代码

echocli.c【客户端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
复制代码

对于服务器端:echosrv.c

对于客户端:echocli.c

下面经过一个简单的图来描述一下其关系:

可想而知,这两个套接字都有本身的地址,对于conn服务端而言,它的地址是在绑定的时候确认的,也就是:

而对于sock客户端而言,它的地址是在链接成功时肯定的。一旦链接成功,则会自动选择一个本机的地址和端口号,当一个客户端链接服务器成功时,在服务器端是能够打印出客户端的地址和端口信息的,具体代码以下:

因此能够将其客户端的地址和端口号打印出来,修改服务端代码以下:

此次编译运行看下效果:

当客户端链接成功时,则在服务端将其客户端的ip和端口号打印出来了。

接下来,要解决一个上篇博文中遇到的问题,问题现象就是以下:

缘由是因为,重启时会从新再绑定,而此时该服务器是处于TIME_WAIT状态,经过命令能够查看到该状态:

【注意】:TIME_WAIT状态,需服务器与客户端都已经退出来才会出来。

而该状态下,默认是没法再次绑定的,那如何解决此问题呢?可使用SO_REUSEADDR选项来解决此问题:

具体使用方法以下,修改服务端代码以下:

这时再来看是否解决了此问题:

可是,这个错误还会在一种场景下报出来,以下:

下面再来看一下目前程序的问题:目前服务器只能接收一个客户端的链接,看下面:

分析一下服务端的代码就能够得知:

解决这个问题的思路就是:一个链接一个进程来处理并发(process-per-connection),也就是上面画红圈的放到子进程去处理,而后主进程能够去accept客户端的请求了,具体代码修改以下:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void do_service(int conn)//将处理客户端请求数据逻辑封装到一个函数中,这样代码更加模块化
{
    char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            fputs(recvbuf, stdout);
            write(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)//用一个循环来不断处理客户端的请求,因此将accept操做也放到循环中
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();//建立进程
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {//子进程来处理与客户端的数据
            close(listenfd);//对于子进程而言,监听listenfd套接字没用,则直接关闭掉,注意:套接字在父子进程是共享的
            do_service(conn);//开始处理请求数据
        }
        else//父进程则回到while循环头,去accept新的客户端的请求,这样就比较好的解决多个客户端的请求问题
            close(conn);
    }
    
    return 0;
}
复制代码

下面来看下效果:

对于这段程序,其实还须要完善,也就是不能监听客户端的退出,

那怎么监听客户端的监听呢?

编译运行看一下效果:

从中能够看到,当客户端退出时,服务端也收到消息了。

下面用多进程方式实现点对点聊天来进一步理解套接字编程,这里实现的聊天程序是这样:

那无论对于服务端,仍是客户端,应该每一个端点都有有两个进程,一个进程读取对等方的数据,另外一个进程专门从键盘中接收数据发送给对待方,这里实现的只是一个服务端跟一个客户端的通信,不考虑一个服务端跟多个客户端的通信了,重在练习,下面仍是基于以前的代码开始实现:

p2psrv.c【点对点通讯服务端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {//子进程用来从键盘中获取输入输数据向客户端发送数据
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        exit(EXIT_SUCCESS);
    }
    else
    {//父进程读取从客户端发送过来的数据
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
复制代码

基本上前面的listen、bind、accept的代码没动,编译一下:

p2pcli.c【点对点通讯客户端】:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)


int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid == 0)
    {//子进程接收来自服务端发来的数据
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        close(sock);
    }
    else
    {//父进程获取从键盘中敲入的命令向服务端发送数据
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        close(sock);
    }
    return 0;
}
复制代码

编译运行:

可见就实现了点对点的聊天程序。这时客户端关闭时,服务端也关闭了,可是,实际上服务端的程序仍是有些问题的,分析一下代码:

能够在父子进程退出时都打印一个log来验证下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        printf("parent close\n");
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
复制代码

而此时,若是我按任意一个字符,则子进程就退出来:

那怎么解决当父进程退出时,也让其子进程退出呢,就能够用到咱们以前学过的信号来解决了,具体以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)//当子进程收到信号时,则将自身退出
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
        ERR_EXIT("accept");

    printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));


    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");
    
    if (pid == 0)
    {
        signal(SIGUSR1, handler);//子进程注册一个SIGUSR1信号,以便在父进程退出时,通知子进程退出
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(conn, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        printf("child close\n");
        exit(EXIT_SUCCESS);
    }
    else
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(conn, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        printf("parent close\n");
        kill(pid, SIGUSR1);//当父进程退出时,发送一个SIGUSR1信号
        exit(EXIT_SUCCESS);
    }
    
    return 0;
}
复制代码

这时再看下效果:

若是反过来呢,将服务端关闭,那客户端程序也会关闭么?

能够看到,当服务端退出时,客户端并无退出,那是啥缘由呢?

那解决此问题也是能够利用信号来解决,就像服务端同样,能够在子进程退出时,向父进程发送一个信号,而后父进程也退出既可,修改代码以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void handler(int sig)
{
    printf("recv a sig=%d\n", sig);
    exit(EXIT_SUCCESS);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    pid_t pid;
    pid = fork();
    if (pid == -1)
        ERR_EXIT("fork");

    if (pid == 0)
    {
        char recvbuf[1024];
        while (1)
        {
            memset(recvbuf, 0, sizeof(recvbuf));
            int ret = read(sock, recvbuf, sizeof(recvbuf));
            if (ret == -1)
                ERR_EXIT("read");
            else if (ret == 0)
            {
                printf("peer close\n");
                break;
            }
            
            fputs(recvbuf, stdout);
        }
        close(sock);
        kill(getppid(), SIGUSR1);
    }
    else
    {
        signal(SIGUSR1, handler);
        char sendbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
            write(sock, sendbuf, strlen(sendbuf));
            memset(sendbuf, 0, sizeof(sendbuf));
        }
        close(sock);
    }


    
    return 0;
}
复制代码

这里就很少解释了,跟服务端的道理同样,这样再来运行一下看下效果:

流协议与粘包

关于什么是粘包可能有些抽象,先得有一些理论基础:咱们知道TCP是一个基于字节流的传输服务,这意味着TCP所传输的数据之间是无边界的,像流水同样,是没法区分边界的;而UDP是基于消息的传输服务,它传输的是数据报文,是有边界的。

而对于数据之间有无边界,反映在对方接收程序的时候,是不同的:对于TCP字节流来讲,对等方在接收数据的时候,不可以保证一次读操做,可以返回多少个字节,是一个消息,仍是二个消息,这些都是不肯定的;而对于UDP消息服务来讲,它可以保证对等方一次读操做返回的是一条消息。

因为TCP的无边界性,就会产生粘包问题,那粘包问题具体体现是怎样的呢?下面用图来进行阐述:

假设主机A(Host A)要向主机B(Host B)发送两个数据包:M1,M2

而对于对待接收方主机B来讲,可能会有如下几种状况:

也就是第一次读操做恰好返回第一条消息(M1)的所有,接下来第二次读操做返回第二条消息(M2)的所有,因此这就没有粘包问题。

一次读操做就返回了M1,M2的全部,这样M1和M2就粘在一块儿了,这就能比较直观的体会到粘包的表现了。

一次读操做返回了M1的所有,而且还有M2的一部分(m2_1);第二次读操做返回了M2的另一部分(M2_2)。

一次读操做返回了M1的一部分(M1_1);第二次读操做返回了M1的另一部分(M1_2),而且还有M2的所有。

固然除了上面四种状况,可能还存在其它组合,由于主机B一次能接收的字节数是不肯定的。

下面来探讨下产生的缘由。

粘包产生的缘由

① 应用程要将本身缓冲区中的数据发送出去,首先要调用一个write方法,将应用程序的缓冲区的数据拷贝到套接口发送缓冲区(SO_SNDBUF),而该缓冲区有一个SO_SNDBUF大小的限制,若是应用缓冲区一条消息的大小超过了SO_SNDBUF的大小,那这时候就有可能产生粘包问题,由于消息被分隔了,一部分已经发送给发送缓冲区,且对方已经接收到了,另一部分才放到了发送缓冲区,这样对方就延迟接收了消息的后一部分。这就致使了粘包问题的出现。

②TCP传输的段有最大段(MSS)的限制,因此也会对应用发送的消息进行分割而产生粘包问题。

③链路层它所传输的数据有一个最大传输单元(MTU)的限制,若是咱们所发送的数据包超过了最大传输单元,会在IP层进行分组,这也可能致使消息的分割,因此也有可能出现粘包问题。

固然还有其它缘由,如TCP的流量控制、拥塞控制、TCP的延迟发送机制,对于上面说的理论理解起来比较抽象,只要记住一条:TCP会产生粘包问题既可。

粘包解决方案

既然TCP协议没有在传输层没有维护消息与消息之间的边界,因此:

咱们所要发送的消息是一个定长包,那么对等方在接收的时候已定长的方式来进行接收,就能确保消息与消息之间的边界。

这种方式有个问题,就是若是消息自己就带这些字符的话,就没法就没法区分消息的边界了,这时就须要用到转义字符了。

其中包头是定长的,如4个字节。

这些解决方案有一个很重要的问题,就是定长包的接收,咱们以前说了,TCP是一个流协议,它不能保证对方一次接收接收到了多少个字节,那咱们就须要封装一个函数:接收肯定字节数的读操做。

下面来封装两个函数,以下:

readn、writen

接收确切数目的读操做

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void do_service(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = read(conn, recvbuf, sizeof(recvbuf));
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                write(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
复制代码

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        write(sock, sendbuf, strlen(sendbuf));
        read(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
复制代码

对于这个函数的封装,仍是参考这个原形来设计,参数保持同样:

这样,最后用咱们写的函数来替换这个系统调用既可,下面则正式开始封装此函数:

ssize_t readn(int fd, void *buf, size_t count)//读取count个字节数,其中size_t是无符号的整数,ssize_t是有符号的整数
{
    size_t nleft = count;//剩余的字节数
    ssize_t nread;//已接收的字节数
    char *bufp = (char*)buf;

    while (nleft > 0)
    {//因为不能保证一次读操做可以返回字节数是多少,因此须要进行循环来接收
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)//被信号中断了,则继续执行,由于不是出错
                continue;
            return -1;//表示读取失败了
        }
        else if (nread == 0)//对等方关闭了
            return count - nleft;//返回已经读取的字节数

        bufp += nread;
        nleft -= nread;
    }

    return count;
}
复制代码

【说明】:关于这个函数的编写,能够好好理解下,目的就是用咱们本身封装的方法来代替系统的读方法。

发送确切数目的写操做

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)//若是是这种状况,则表示什么都没发生,继续还得执行
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}
复制代码

接下来,用咱们本身封装的函数来代码系统函数,先只修改客户端程序,一步步来引导其这样作的缘由。

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

void do_service(int conn)
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readn(conn, recvbuf, sizeof(recvbuf));//将其替换成本身封装的方法
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        else if (ret == -1)
            ERR_EXIT("read");
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, ret);
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
复制代码

这时客户端程序还保持原样,这时编译,那会有什么效果呢:

发现,这时客户端发送的数据服务端没有办法接收了,这是为何呢?

若是对方发送数据不足1024个字节时,那就会一直循环,查看其readn函数:

这时,解决方案,第一种就是发送定长包:

因此,这时将客户端的write替换成writen,以下:

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)//须要将函数的定义也挪过来
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] ={0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sock, sendbuf, sizeof(sendbuf));
        readn(sock, recvbuf, sizeof(recvbuf));

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock);
    
    return 0;
}
复制代码

这时再运行,看问题是否解决:

这时就解决了以前的问题,可是有一个问题,每次发送都是1024定长的字节,若是只发送几个字节的内容也会占用这么多字节,这就会增长网络的负担,那怎么解决这个问题呢?

这时候须要本身定义一个协议,能够定义这样一个包的结构:

这时,在发送数据时,就得进行相应的修改,以下:

这时,服务端接收数据时,也须要进行修改:

当服务端接收完以后,接着回显给客户端,代码修改以下:

这时客户端接收也同理,也是先读取包长度,而后再接收包数据,修改以下:

至此,就已经将解决定长字长的问题的代码写完了,下面来编译运行一下:

至此,这样就很好的解决了粘包问题,在局域网中是不可能出现粘包问题的,可是若是将程序放到广域网,若是不处理粘包问题会存在很大问题的。

对于以前写的回射客户/服务器端的程序中,咱们是用的read和write来读取和发送数据的,以下:

那recv相对于read有什么区别呢?先看一下man帮助:

其实它跟read函数功能同样,均可以从套接口缓冲区sockfd中取数据到buf,可是recv仅仅只可以用于套接口IO,并不能用于文件IO以及其它的IO,而read函数能够用于任何的IO;

recv函数相比read函数多了一个flags参数,经过这个参数能够指定接收的行为,比较有用的两个选项是:

关于这个选项,先作下了解。

这个此次要学习的,它能够接收缓冲区中的数据,可是并不从缓冲区中清除,这是跟read函数有区别的地方,read函数一旦读取了,就会直接从缓冲区中清除。

下面用recv函数来封装一个recv_peek函数,仍是继上节中的程序进行修改:

注意:这时缓冲区中的数据还在,下面利用这个封装的函数来实现readline。

也就是实现按行读取,读取直到\n字符,实际上,它也能解决上节中提到的粘包问题,回顾下上节的粘包问题解决方案:

咱们只要解释\n为止,表示前面是一个条合法的消息,对于readline的实现,能够有三种方案:

①、最简单的方案就是一个字符一个字符的读取,而后作判断是否有"\n",可是这种效率比较低,由于会屡次掉用read或recv系统函数。

②、用一个static变量保存接收到的数据进行缓存,在下次时从这个缓存变量中读取而后估"\n"判断。可是一旦用到了static变量,这意味着用到的函数是不可重录函数

③、偷窥的方法,也就是此次要采用的方案。下面就利用咱们封装的recv_peek函数实现readline:

首先用recv从缓冲区中偷窥数据:

接着判断是否有"\n"字符,而后将其从缓冲区中读出来,而且清空缓冲区:

若是没有找到"\n"字符,则读取数据并清空缓存,并不断循环,具体以下:

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已读字节数
    char *bufp = buf;
    int nleft = maxline;//剩余字节数
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//证实读取失败了
            return ret;
        else if (ret == 0)//证实是对方关闭了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因为readn中是用的read函数读取,因此读取数据以后会将清空缓冲区,也正好须要这样
                if (ret != i+1)//若是读出来的字符数不等于i+1,则说明读取失败了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//这种状况说明也是读取有问题的
            exit(EXIT_FAILURE);

        //执行到此则说明没有找到"\n"字符,这时读取数据而后清空缓冲区
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再继续读后面的
    }

    return -1;//执行到此,则说明失败了
}
复制代码

下面则用这个readline方法来解决回射客户/服务端粘包问题,因为是按一行一行发送数据,说明消息之间的边界就是"\n",因此对于以前封装的定长包结构的方式能够去掉了:

将服务端改为按行读取代码以下:

对于客户端修改也同理:

首先将咱们封装的readline的两个方法拷贝过来:

而后也改为按行读取:

最后来编译运行:

可见,经过按行读取的方式,也一样达到了回射客户/服务器端的效果,而且也解决了粘包问题。

下面贴出服务端与客户端修改后的完整代码:

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已读字节数
    char *bufp = buf;
    int nleft = maxline;//剩余字节数
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//证实读取失败了
            return ret;
        else if (ret == 0)//证实是对方关闭了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因为readn中是用的read函数读取,因此读取数据以后会将清空缓冲区,也正好须要这样
                if (ret != i+1)//若是读出来的字符数不等于i+1,则说明读取失败了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//这种状况说明也是读取有问题的
            exit(EXIT_FAILURE);

        //执行到此则说明没有找到"\n"字符,这时读取数据而后清空缓冲区
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再继续读后面的
    }

    return -1;//执行到此,则说明失败了
}

void do_service(int conn)
{
    char recvbuf[1024];
    while (1)
    {
        memset(recvbuf, 0, sizeof(recvbuf));
        int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        fputs(recvbuf, stdout);
        writen(conn, recvbuf, strlen(recvbuf));
    }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;
    
    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            do_service(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
复制代码

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;//已读字节数
    char *bufp = buf;
    int nleft = maxline;//剩余字节数
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)//证实读取失败了
            return ret;
        else if (ret == 0)//证实是对方关闭了
            return ret;

        nread = ret;

        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);//因为readn中是用的read函数读取,因此读取数据以后会将清空缓冲区,也正好须要这样
                if (ret != i+1)//若是读出来的字符数不等于i+1,则说明读取失败了
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)//这种状况说明也是读取有问题的
            exit(EXIT_FAILURE);

        //执行到此则说明没有找到"\n"字符,这时读取数据而后清空缓冲区
        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;//再继续读后面的
    }

    return -1;//执行到此,则说明失败了
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    {
        writen(sock, sendbuf, strlen(sendbuf));

        int ret = readline(sock, recvbuf, sizeof(recvbuf));
        if (ret == -1)
                ERR_EXIT("readline");
        else if (ret == 0)
        {
                printf("client close\n");
                break;
        }

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }
    
    return 0;
}
复制代码

getsockname:获取套接口本地的地址

当客户端成功与服务端链接以后,若是想知道客户端的地址,就能够经过它来获取,修改代码以下:

而后编译运行:

getpeername:获取对等方的地址

因为它的使用方法跟getsockname同样,这里就不说明了,注意:sockfd需是链接成功的套接口,另外对于服务端获取客户端ip,像这种状况下也需用这个接口来得到:

gethostname:获取主机的名称

gethostbyname:经过主机名来获取主机上全部的ip地址

下面利用上面的函数,来获取主机上全部的ip地址:

编译运行:

查看man:

因而加入该头文件:

再次编译:

可能本地ip列表有多个,可是通常来讲默认本机ip都是第一个,因此,对于得到本机ip能够将其封装成一个方法,便于以后直接调用,以下:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

int getlocalip(char *ip)
{
    char host[100] = {0};
    if (gethostname(host, sizeof(host)) < 0)
      return -1;
    struct hostent *hp;
    if ((hp = gethostbyname(host)) == NULL)
      return -1;
    strcpy(ip, inet_ntoa(*(struct in_addr*)hp->h_addr_list[0]));
    return 0;

}

int main(void)
{
    char host[100] = {0};
    if (gethostname(host, sizeof(host)) < 0)
        ERR_EXIT("gethostname");

    struct hostent *hp;
    if ((hp = gethostbyname(host)) == NULL)
        ERR_EXIT("gethostbyname");

    int i = 0;
    while (hp->h_addr_list[i] != NULL)
    {
        printf("%s\n", inet_ntoa(*(struct in_addr*)hp->h_addr_list[i]));
        i++;
    }
    
    char ip[16] = {0};
    getlocalip(ip);
    printf("localip=%s\n", ip);
    return 0;
}
复制代码

编译运行:

另外,经过man帮助能够查看到一点:

那获得的信息就能够将上面得到默认地址用它进行替换:

回顾一下咱们之间实如今TCP回射客户/服务器程序,首先回顾一下第一个版本:

TCP客户端从stdin获取(fgets)一行数据,而后将这行数据发送(write)到TCP服务器端,这时TCP服务器调用read方法来接收而后再将数据回射(write)回来,客户端收到(read)这一行,而后再将其输出fputs标准输出stdout,可是这个程序并无处理粘包问题,由于TCP是流协议,消息与消息之间是没有边界的,为了解决这个问题,因而第二个改进版程序诞生了:

一行一行的发送数据,每一行都有一个\n字符,因此咱们在服务器端实现了按行读取的readline方法,另外发送也并不能保证一次调用write方法就将tcp应用层的全部缓冲区拷贝到了套接口缓冲区,因此咱们封装了一个writen方法进行一个更可靠消息的发送,因此就很好的解决了粘包问题。

echosrv.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nread;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nread = read(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nread == 0)
            return count - nleft;

        bufp += nread;
        nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
    size_t nleft = count;
    ssize_t nwritten;
    char *bufp = (char*)buf;

    while (nleft > 0)
    {
        if ((nwritten = write(fd, bufp, nleft)) < 0)
        {
            if (errno == EINTR)
                continue;
            return -1;
        }
        else if (nwritten == 0)
            continue;

        bufp += nwritten;
        nleft -= nwritten;
    }

    return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
    while (1)
    {
        int ret = recv(sockfd, buf, len, MSG_PEEK);
        if (ret == -1 && errno == EINTR)
            continue;
        return ret;
    }
}

ssize_t readline(int sockfd, void *buf, size_t maxline)
{
    int ret;
    int nread;
    char *bufp = buf;
    int nleft = maxline;
    while (1)
    {
        ret = recv_peek(sockfd, bufp, nleft);
        if (ret < 0)
            return ret;
        else if (ret == 0)
            return ret;

        nread = ret;
        int i;
        for (i=0; i<nread; i++)
        {
            if (bufp[i] == '\n')
            {
                ret = readn(sockfd, bufp, i+1);
                if (ret != i+1)
                    exit(EXIT_FAILURE);

                return ret;
            }
        }

        if (nread > nleft)
            exit(EXIT_FAILURE);

        nleft -= nread;
        ret = readn(sockfd, bufp, nread);
        if (ret != nread)
            exit(EXIT_FAILURE);

        bufp += nread;
    }

    return -1;
}

void echo_srv(int conn)//因为这个函数的意义就是回显消息给客户端,因此改一个函数名
{
    char recvbuf[1024];
        while (1)
        {
                memset(recvbuf, 0, sizeof(recvbuf));
                int ret = readline(conn, recvbuf, 1024);
        if (ret == -1)
            ERR_EXIT("readline");
        if (ret == 0)
        {
            printf("client close\n");
            break;
        }
        
                fputs(recvbuf, stdout);
                writen(conn, recvbuf, strlen(recvbuf));
        }
}

int main(void)
{
    int listenfd;
    if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
/*    if ((listenfd = socket(PF_INET, SOCK_STREAM, 0)) < 0)*/
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    /*servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");*/
    /*inet_aton("127.0.0.1", &servaddr.sin_addr);*/

    int on = 1;
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
        ERR_EXIT("setsockopt");

    if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("bind");
    if (listen(listenfd, SOMAXCONN) < 0)
        ERR_EXIT("listen");

    struct sockaddr_in peeraddr;
    socklen_t peerlen = sizeof(peeraddr);
    int conn;

    pid_t pid;
    while (1)
    {
        if ((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
            ERR_EXIT("accept");

        printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));

        pid = fork();
        if (pid == -1)
            ERR_EXIT("fork");
        if (pid == 0)
        {
            close(listenfd);
            echo_srv(conn);
            exit(EXIT_SUCCESS);
        }
        else
            close(conn);
    }
    
    return 0;
}
复制代码

echocli.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

ssize_t readn(int fd, void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nread;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nread = read(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nread == 0)
                        return count - nleft;

                bufp += nread;
                nleft -= nread;
        }

        return count;
}

ssize_t writen(int fd, const void *buf, size_t count)
{
        size_t nleft = count;
        ssize_t nwritten;
        char *bufp = (char*)buf;

        while (nleft > 0)
        {
                if ((nwritten = write(fd, bufp, nleft)) < 0)
                {
                        if (errno == EINTR)
                                continue;
                        return -1;
                }
                else if (nwritten == 0)
                        continue;

                bufp += nwritten;
                nleft -= nwritten;
        }

        return count;
}

ssize_t recv_peek(int sockfd, void *buf, size_t len)
{
        while (1)
        {
                int ret = recv(sockfd, buf, len, MSG_PEEK);
                if (ret == -1 && errno == EINTR)
                        continue;
                return ret;
        }
}


ssize_t readline(int sockfd, void *buf, size_t maxline)
{
        int ret;
        int nread;
        char *bufp = buf;
        int nleft = maxline;
        while (1)
        {
                ret = recv_peek(sockfd, bufp, nleft);
                if (ret < 0)
                        return ret;
                else if (ret == 0)
                        return ret;

                nread = ret;
                int i;
                for (i=0; i<nread; i++)
                {
                        if (bufp[i] == '\n')
                        {
                                ret = readn(sockfd, bufp, i+1);
                                if (ret != i+1)
                                        exit(EXIT_FAILURE);

                                return ret;
                        }
                }

                if (nread > nleft)
                        exit(EXIT_FAILURE);

                nleft -= nread;
                ret = readn(sockfd, bufp, nread);
                if (ret != nread)
                        exit(EXIT_FAILURE);

                bufp += nread;
        }

        return -1;
}

void echo_cli(int sock)//将以前这一段代码封装成一个函数,回显客户端,让其代码更加整洁。
{
    char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {
                writen(sock, sendbuf, strlen(sendbuf));

                int ret = readline(sock, recvbuf, sizeof(recvbuf));
                if (ret == -1)
                        ERR_EXIT("readline");
                else if (ret == 0)
                {
                        printf("client close\n");
                        break;
                }

                fputs(recvbuf, stdout);
                memset(sendbuf, 0, sizeof(sendbuf));
                memset(recvbuf, 0, sizeof(recvbuf));
        }

        close(sock);
}

int main(void)
{
    int sock;
    if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
        ERR_EXIT("socket");

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
        ERR_EXIT("connect");

    struct sockaddr_in localaddr;
    socklen_t addrlen = sizeof(localaddr);
    if (getsockname(sock, (struct sockaddr*)&localaddr, &addrlen) < 0)
        ERR_EXIT("getsockname");

    printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));


    echo_cli(sock);

    return 0;
}
复制代码

运行效果以下:

对于上面运行程序,当客户端退出以后,服务端会产生僵进程:

对于僵进程的避勉,有以下方法,以前都有介绍过:

忽略SIGCHLD信号,因此,能够在服务端加入:

编译运行,看是否解决了僵进程的问题:

第二种方式,则能够捕捉SIGCHLD信号来进行忽略,具体代码以下:

此时再编译运行:

可是,对于上面两种解决方式仍是会存在一些问题,若是有不少个子进程同时退出,wait函数并不能等待全部子进程的退出,由于wait仅仅只等待第一个子进程退出就返回了,这时就须要用到waitpid了,在这以前,须要模拟一下由五个客户端并发链接至服务器,并同时退出的状况,用简单示例图来描述以下:

客户端建立五个套接字来链接服务器,一旦链接了服务器就会建立一个子进程出来为客户端进行处理,从图中能够看,服务端建立了5个子进程出来。

对于服务端程序是不须要进行修改的,只需修改客户端建立五个套接字既可,修改客户端程序以下:

这时,编译运行:

关于这个比较容易理解,是因为wait函数只等待一个子进程退出就返回了,因此有四个进程处于僵尸的状态。

再运行一次:

那若是再运行一次呢?

从以上结果来看,僵尸进程的数目不必定。

因为wait函数只能返回一个子进程,这时候咱们应该用什么方法解决呢?能够用waitpid函数来解决,具体程序修改以下:

再来运行看是否还存在僵尸进程呢?

这种状况有一点不太好解释,可是还会遇到这种状况:

对于这种状况,就能够用理论来解释了,缘由是因为有五个子进程都要退出,这就意味着有五个信号要发送给父进程,

在关闭链接时,会向服务器父进程发送SIGCHLD信号,具体以下图:

此时父进程处于一个handle_sigchld()的过程,而在这个处理过程当中,若是其它信号到了,其它信号会丢失,以前也提到过,这些信号是不可靠信号,不可靠信号只会排队一个,若是只排队一个的话,那么最终可以处理两个子进程,因此,最后存在三个僵尸进程。

那为何有时会有2个僵进程,有时又会有四个呢,这里来解释一下:

缘由可能跟FIN(客户端在终止时,会向服务器发送FIN)到达的时机有关,服务器收到FIN的时候,返回等于0就退出了子进程,这时就会向服务器发送SIGCHLD信号,若是这些信号都是同时到达的话,那么就有可能只处理一个,这时就会出现了4个僵尸进程;若是不是同时到达,handle_sigchld()函数就会执行屡次,若是被执行了两次,捕捉到了两个信号的话,那就此时就会出现3个僵进程,以此类推,但无论结果怎样,都是属于须要解决的状况。

那怎么解决此问题呢,能够用一个循环来作:

这时再编译运行:

经过这个状态的学习,进一步复习一下“链接创建三次握手、链接终止四次握手【下面会分别来介绍】”,下面首先来看一张图:

从图中能够数一下,总共有“LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_一、CLOSE_WAIT、FIN_WAIT_二、LAST_ACK、TIME_WAIT、CLOSED”十个状态,那为啥标题上说有十一个呢?其实还有一个状态叫CLOSING,这个状态产生的缘由比较特珠,咱们以后再来看它,下面先来分别梳理一下链接创建三次握手和链接终止四次握手状态流程:

链接创建三次握手

LISTEN

首先服务端建立一个socket,这时它的状态其实是CLOSED状态,也就是最后一种状态,虽然没有标识出来:

一旦咱们调用bind、listen函数:

这时就处于LISTEN状态,以下:

这时候的套接口就称为被动套接口,这意味着这个套接口不能用于发起链接,只能用来接受链接,这个以前都有介绍,

而这时回到客户端来讲:

SYN_SENT:

当建立套接口时,也是一个CLOSED状态,这里也未标明,接着再调用connect进行主动打开,这时的套接口就称为主动套接口,它能够用来发起链接的:

这时的状态为SYN_SENT,这时TCP会传输一个发起链接的TCP段"SYN a"这个段给服务器端,以下:

而此时服务端调用了accept方法处理阻塞的状态:

可是TCP协议栈会收到“SYN a”TCP段,这时就处于SYN_RCVD状态:

当收到SYN_RCVD以后,TCP会对序号“SYN a”进行确认,发起一个"ACK a+1"TCP段给客户端,而且也有一个"SYN b"序号,以下:

对于客户端来讲,收到"ACK a+1"TCP段以后,就处于"ESTABLISHED"链接的状态,这时connect函数就可以返回;

而对于服务端来讲并未处理链接的状态,它须要等到客户端再次发送"ACK b+1"TCP段,这就是链接创建的三次握手,服务端收到这个TCP段以后,则也会处于"ESTABLISHED"状态,以下:

它实际上会将未链接队列当中的一个条目移至已链接队列当中,这时accept就能够返回了,由于它能够从已链接队列的队头返回第一个链接,以下:

链接终止四次握手

当客户端发起关闭请求,这时会向服务器端发起一个"FIN x ACK y"的TCP段给对方,这时客户端的状态就叫做FIN_WAIT_1,以下:

这时服务器端收到一个终止的TCP段,这时read就会返回为0,实际上当服务端收到这个TCP段以后,服务端会对它进行确认,则会向客户端发送"ACK+1"的TCP段,这时服务端的状态为CLOSE_WAIT:

客户端的状态为FIN_WAIT_2:

这时候,客户端就处于TIME_WAIT状态:

注意:这个状态要保留2倍的MSL(tcp最大的生命期)时间,为何呢,这个能够简单说明一下,是因为最后一个"ACK y+1"发送过去,不能肯定对方收到了,这个ACK可能会丢失,有了这个时间的存在就确保了能够重传ACK,固然还有其它的缘由,这里先了解一下既可,当服务器收到了最后一个确认之后,则就处于CLOSED状态了:

注意:服务端处于CLOSED状态,并不表明发送关闭的这一端(就是客户端)就处于CLOSED状态,需等到2倍的MSL时间消失之后才处理CLOSED状态。

以上是TCP的十点状态,还有一个特珠状态叫CLOSING,它产生的缘由是:双方同时关闭,以下图:

具体流程是这样的:客户端和服务端同时调用close,这时客户端和服务端都处于FIN_WAIT_1状态

这时,双方都会发起FIN TCP段,

这时须要对其进行段确认:

这时状态则称为CLOSING状态,这时就不会进行到FIN_WAIT_2这种状态了。

一旦收到对方的ACK,则会处于TIME_WAIT状态:

可见TIME_WAIT状态是主动关闭的一方才产生的状态。

说了这么多理论,下面用代码来进行论证,以便增强理解,仍是用以前的服务端/客户端回显的例子,首先启动服务端,这时查看下状态:

接着启动一个客户端,发起链接:

因为目前作实验是在同一台机器上进行的,因此打印了三个状态,实际上应该是服务端的状态和客户端的状态是分开的。

【注意】:因为在运行时这两个状态SYN_SENT、SYN_RCVD过快,因此看不到。

下面来看下链接终止的状态,先关闭服务端,首先找到服务端的进程,经过kill掉的办法来关闭服务端:

杀掉服务端进程来模拟服务端的close:

【注意】:这里的服务端进程是指与客户端通信的进程。

这时查看一下状态:

为何不会处于TIME_WAIT状态呢?

缘由在于,read函数没有机会返回0:

这时应该查看一下客户端的程序才知道问题,客户端此时是阻塞在fgets函数来键盘的消息:

这就意味着客户端这个进程没有机会调用close,因此服务器端没法进入TIME_WAIT,它只能保留在FIN_WAIT_2状态了

这时候再来看一下状态:

只有LISTEN状态了,这是为何呢?

仍是得回到客户端的程序来分析,因为从键盘敲入了字符,因此:

这时就会走以下流程:

而因为服务器的进程已经杀掉了,因此说不会显示TIME_WAIT状态了。

【注意】:该实验在最后会阐述一个SIGPIPE的信号问题。

若是先关闭客户端,这时就会看到这个状态了,以下:

另外这个状态上面也提到了,会保留2倍的MSL时间才会消失,若是服务器端保留两倍的MSL时间,这时候就会致使服务器端没法从新启动,若是没有调用SO_REUSEADDR话。

SIGPIPE信号产生的缘由

对于上面作的一个实验,就是服务端先关闭以后,而后客户端还能够向服务端发起数据,这是因为客户端收到FIN仅仅表明服务端不能发送数据了,以下图:

而若是发送数据给对方,可是对方进程又已经不存在,会致使对方发送一个RST重启TCP段给发送方(这里指的就是上面作实验的客户端),可是在收到RST段以后,若是再调用write就会产生SIGPIPE信号,而产生该信号默认就会终止程序,下面来修改一下客户端的代码,仍是基于上面的实验,以下:

这时,再来看一下效果:

首先运行客户端与服务端:

而后将服务端与客户端的进程找到,并杀掉来模拟关闭服务端:

而后这时在客户端中敲入字符,并回车,看下结果:

结合代码来看一下:

为了证实确实是收到了SIGPIPE信号,咱们捕捉一下该信号,修改代码以下:

再次编译运行,这一次运行步骤还跟上次同样,须要先杀掉父进程,而后再在客户端敲入一个字符,这里就不说明了,只看一下结果:

实际上,对于这个信号的处理咱们一般忽略便可,能够加入这条语句:signal(SIGPIPE, SIG_IGN);

修改代码以下:

实际上,对于SIGPIPE信号在学习管道时有说过它的产生,若是没有任何读端进程,而后往管道当中写入数据,这时候就会出现段开的管道,而对于TCP咱们能够当作是一个全双工的管道,当某一端收到FIN以后,它并不能确认对等方的进程是否已经消失了,由于对方调用了close并不意味着对进程就已经消失了,用图来理解:

这时候,就须要客户调用一次write,此次并不会产生断开的管道,发现对等方的进程不存在了,则对等方就会发送RST段给客户端,这就意味着全双工管道的读端进程不存在了,因此说若是再次调用write,就会致使SIGPIPE信号的产生,因此说能够利用管道来理解它。

相关文章
相关标签/搜索