• Linux C网络通信过程


    socket函数、sockaddr_in结构体 和 bind函数

    socket函数的作用是创建一个网络文件描述符,程序通过这个文件描述符将数据发送到网络,也通过这个文件描述符从网络中接受数据。观察一下socket函数:

    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0)
    
    • 1
    • 2

    会发现这个函数有三个参数,其中前两个参数指定了底层协议族为AF_INET(进行本地域通信),传输层使用SOCK_STREAM(字节流协议),即TCP协议。

    我们知道,在传输层中网络是通过套接字(ip,端口)来进行定位的,但是socket中并没有指定套接字。默认情况下,系统会会随意分配一个端口,使用本机的ip地址。

    这在客户端是没有问题的,客户端可以选择任意的端口和服务器进行通信。但是,服务器不行,因为客户端是主动向服务器发送数据的,它需要知道数据应该发送到服务器的哪个端口。所以需要服务器事先指定好端口号,服务器通过这个端口向客户端发送数据,也通过这个端口接收客户端发来的数据。

    由此,需要一个套接字结构体sockaddr_in来定义套接字:

    struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;                // 协议族,在socket编程中只能是AF_INET。
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
    servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这个套接字中,除了ip和端口,也指定了协议族,该协议族应该和传递给socket函数的一致。这里INADDR_ANY表示本机的任意ip地址,因为有些服务器不止一块网卡,多网卡的情况下,用该参数表示所有网卡的ip地址。ip地址也可以通过servaddr.sin_port = htons(atoi(argv[1]))的方式指定特定ip地址。

    好了,现在有了套接字,但是和先前定义的socket文件描述符没有关系,所以还需要通过bind函数进行绑定:

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    
    • 1

    这个过程也叫socket命名。

    上面的过程配置的是服务器的套接字,对于服务器来说,这是数据发送的起点,要将数据发送给客户端,还需要知道数据的终点,也就是客户端的套接字。客户端的套接字可以用accept函数获取。

    listen监听队列、accept函数 及 数据的收发

    当服务器得到socket文件描述符后,就可以准备和用户进行通信了。由于一般有多个客户端,所以服务器会通过一个监听队列来保存用户的连接请求。该监听队列通过listen函数来创建:

    listen(listenfd, 5);
    
    • 1

    这里listen的是服务器的listenfd套接字,对客户端来说,这是数据的终点,所以服务器可以针对这个套接字进行监听。当有数据发送到这个套接字的时候,服务器将这个连接请求放入到listen监听队列。

    当服务器要处理客户端连接请求时,通过accept函数选择一个连接。客户端发送的请求到达服务器后,服务器可以从报文的报头部分获得客户端的ip地址和端口号。
    创建一个客户端套接字,服务器将这些信息保存到该套接字中,以便后续通信中将数据发送到客户端套接字。

    int clientfd;                             // 客户端的socket。
    int socklen = sizeof(struct sockaddr_in); // struct sockaddr_in的大小
    struct sockaddr_in clientaddr;            // 客户端的地址信息。
    clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
    
    • 1
    • 2
    • 3
    • 4

    需要注意的是,accept是阻塞式的,当监听队列中没有请求连接时,accept将进入阻塞状态。

    这就意味着,在传统的网络通信模型中,单线程的服务器将只能处理一个连接请求,只有当前的请求完成后,才能处理下一个请求,这是串行的处理方式。

    梳理一下:服务器在处理客户端请求的过程中,将涉及到两个套接字,服务器通过监听本地的套接字来获取连接请求;之后通过accept获取客户端套接字,进行数据的收发,所以在进行数据收发时,只需要用到客户端套接字clientfd

    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));
    recv(clientfd, buffer, sizeof(buffer), 0);	\\接收数据
    strcpy(buffer, "ok");
    send(clientfd, buffer, strlen(buffer), 0);	\\发送数据
    
    • 1
    • 2
    • 3
    • 4
    • 5

    连接的释放

    当客户端主动释放连接后,客户端和服务器之间将进行四次挥手,其中第四次挥手可以通过netstat -nt | grep port观察到

    比如下面的服务器程序,该程序的功能是接受客户端发送的信息,再将信息发送回去。

    /*
     * 程序名:server.cpp,此程序用于演示socket通信的服务端
     * 作者:C语言技术网(www.freecplus.net) 日期:20190525
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    int main(int argc, char *argv[])
    {
      if (argc != 2)
      {
        printf("Using:./server port\nExample:./server 5005\n\n");
        return -1;
      }
    
      // 第1步:创建服务端的socket,和文件描述符一样
      int listenfd;
      // 初始化时只指定了所用的底层协议族为AF_INET(本地域通信),传输层使用SOCK_STREAM(字节流协议),即TCP协议
      if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) 
      {
        perror("socket");
        return -1;
      }
    
      // 第2步:把服务端用于通信的地址和端口绑定到socket上。
      struct sockaddr_in servaddr; // 服务端地址信息的数据结构。
      memset(&servaddr, 0, sizeof(servaddr));
      servaddr.sin_family = AF_INET;                // 协议族,在socket编程中只能是AF_INET。
      servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 任意ip地址。
      // servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); // 指定ip地址。
      servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。
      if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) != 0)
      {
        perror("bind");
        close(listenfd);
        return -1;
      }
    
      // 第3步:把socket设置为监听模式。
      if (listen(listenfd, 5) != 0) // listen函数创建一个listen监听队列用于存放用户连接!!!
      {
        perror("listen");
        close(listenfd);
        return -1;
      }
      printf("Fininsh listening, try to accept...\n");
    
      // 第4步:接受客户端的连接。
      int clientfd;                             // 客户端的socket。
      int socklen = sizeof(struct sockaddr_in); // struct sockaddr_in的大小
      struct sockaddr_in clientaddr;            // 客户端的地址信息。
    
      // accept从listen监听队列中取出一个用户连接,当监听队列为空时,accept陷入阻塞!!!
      clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
      printf("客户端(%s)已连接。\n", inet_ntoa(clientaddr.sin_addr));
    
      // 第5步:与客户端通信,接收客户端发过来的报文后,回复ok。
      char buffer[1024];
      while (1)
      {
        int iret;
        memset(buffer, 0, sizeof(buffer));
        if ((iret = recv(clientfd, buffer, sizeof(buffer), 0)) <= 0) // 接收客户端的请求报文。
        {
          printf("iret=%d, waitting next connection\n", iret);
          break;
        }
        printf("接收:%s\n", buffer);
    
        strcpy(buffer, "ok");
        if ((iret = send(clientfd, buffer, strlen(buffer), 0)) <= 0) // 向客户端发送响应结果。
        {
          perror("send");
          break;
        }
        printf("发送:%s\n", buffer);
      }
    
      // 第6步:关闭socket,释放资源。
      close(listenfd);
      close(clientfd);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    首先通过gcc -o server ./server.c编译得到可执行文件,再通过./server 1234启动该服务程序后,可以通过telnet 127.0.0.1 1234在本地建立和服务器(本机)的连接。

    随后,通过netstat -nt | grep 1234可以查看连接释放后端口的使用情况,如下:

    tcp        0      0 127.0.0.1:1234          127.0.0.1:60026         TIME_WAIT  
    
    • 1

    和1234有关的连接只有一个,这是客户端到服务器的连接,该连接处于TIME_WAIT状态。对照四次挥手示意图,可以看到客户端的这个连接将会持续2MSL。此时再次通过1234端口来启动服务器将会出现端口占用的提示:

    bind: Address already in use
    
    • 1

    在这里插入图片描述
    简单分析一下,当客户端接受到FIN=1,ACK=1后,客户端知道服务器要释放连接了,于是发送报文ACK=1,在收到该报文之前服务器处于LAST_ACK状态,并未释放连接资源。

    如果网络不好,服务器可能收不到该报文,一直处于LAST_ACK状态,并且把原因归咎为客户端没有收到FIN=1, ACK=1报文,所以服务器会继续发送该报文。此时客户端等待的时间没有超过2MSL并处于TIME_WAIT状态,接受到该报文后他明白服务器的连接还未释放,所以再次发送ACK=1报文,通知服务器正常释放连接,避免服务器资源的消耗

    网络正常的情况下,当2MSL过去后,第四次挥手顺利完成,再次使用netstat -nt | grep 1234查看会发现该连接自动释放掉了。

  • 相关阅读:
    python set()去重的底层原理
    [paper] Attention is all you need 论文浅析
    【JVM】第二篇 JVM内存模型深度剖析与优化
    Docker学习笔记
    【面试经典150 | 数组】买卖股票的最佳时机
    徐亦达机器学习:Kalman Filter 卡尔曼滤波笔记 (一)
    基于R语言的raster包读取遥感影像
    一个 OpenTiny,Vue2 Vue3 都支持!
    深入理解JVM虚拟机第四篇:一些常用的JVM虚拟机
    解决java在idea运行正常,但是打成jar包后中文乱码问题
  • 原文地址:https://blog.csdn.net/GodNotAMen/article/details/128010190