Linux网络编程 ——TCP套接字通信

Linux网络编程------TCP套接字通信

    • [7. TCP 三次握手](#7. TCP 三次握手)
    • [8. TCP 滑动窗口](#8. TCP 滑动窗口)
    • [9. TCP 四次挥手](#9. TCP 四次挥手)
    • [10. TCP 通信并发](#10. TCP 通信并发)
    • [11. TCP 状态转换](#11. TCP 状态转换)
    • [12 端口复用](#12 端口复用)

7. TCP 三次握手

    TCP 是一种 面向连接的 单播协议 ,在发送数据前,通信双方必须在彼此间建立一条连接 。所谓的" 连接",其实是 客户端 和 服务器 的内存里保存的一份关于对方的信息 ,如 IP 地址、端口号等。

    TCP 可以看成是一种字节流 ,它会处理 IP 层 或 以下的层 的 丢包、重复 以及 错误问题。在连接的建立过程中,双方需要交换一些连接的参数 。这些参数可以放在 TCP 头部。

    TCP 提供了一种 可靠、面向连接、字节流、传输层的服务,采用 三次握手 建立一个连接 。采用 四次挥手 来关闭一个连接。

    三次握手 的目的是保证双方 互相之间 建立了连接 。三次握手 发生在 客户端连接 的时候,当调用 connect(),底层会通过TCP协议进行 三次握手。

三次握手:

TCP 三次握手状态转换 (不携带数据):

  • 第一次握手:

    1. 客户端将 SYN 标志位 置为 1
    2. 生成一个随机的32位的序号 seq=J , 这个序号后边是可以携带数据(数据的大小)
  • 第二次握手:

    1. 服务器端接收客户端的连接: ACK=1
    2. 服务器会回发一个确认序号 : ack = 客户端的序号 + 数据长度 + SYN/FIN(按 一个字节 算)
    3. 服务器端会向客户端发起连接请求: SYN=1
    4. 服务器会生成一个随机序号 :seq = K
  • 第三次握手:

    1. 客户单应答服务器的连接请求:ACK=1
    2. 客户端回复收到了服务器端的数据:ack = 服务端的序号 + 数据长度 + SYN / FIN(按 一个字节 算)

为什么要三次握手,而不是两次握手:确认客户端 和 服务器 端都能 收发数据 ,以保证建立的 连接是可靠的。

TCP 头部结构

  • 16 位端口号 (port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统 自动选择 的 临时端口号。

  • 32 位序号 (sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。⭐️

    • 假设 主机 A 和 主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个 随机值 ISN (Initial Sequence Number,初始序号值 )。那么在该传输方向上(从 A -> B),后续的 TCP 报文段中 序号值 将被系统设置成 ISN 加上 该报文段所携带数据的 第一个字节在整个字节流中 的偏移。 例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 B 到 A )的 TCP 报文段的序号值也具有相同的含义。
  • 32 位确认号 (acknowledgement number):用作对另一方 发送来的 TCP 报文段 的响应。其值 是收到的 TCP 报文段的序号值 + 数据长度 / (数据长度+1)。⭐️

    • 假设 主机 A 和 主机 B 进行TCP 通信,那么 A 发送出 的 TCP 报文段 不仅携带自己的序号 ,而且包含对 B 发送来 的 TCP 报文段 的 确认号。
    • 反之,B 发送出 的 TCP 报文段 也同样携带自己的序号 和 对 A 发送来的 报文段 的确认序号 。

    注意:只有收到 标志位 SYN=1 或 FIN=1 时,确认序号ack才会再 +1;其余的都是加上具体 数据长度。

  • 4 位头部长度 (head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是 60 字节。

  • 6 位标志位 包含如下几项:

    • URG 标志,表示 紧急指针(urgent pointer)是否有效。
    • ACK 标志,表示 确认号 是否有效。我们称携带 ACK 标志的 TCP 报文段为 确认报文段。⭐️
    • PSH 标志,提示接收端应用程序应该 立即 从 TCP 接收缓冲区 中 读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
    • RST 标志,表示要求对方 重新建立连接。我们称携带 RST 标志的 TCP 报文段为 复位报文段。
    • SYN 标志,表示 请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为 同步报文段。⭐️
    • FIN 标志,表示通知对方本端要 关闭连接了。我们称携带 FIN 标志的 TCP 报文段为 结束报文段。⭐️
  • 16 位窗口大小 (window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是 接收通告窗口(Receiver Window,RWND )。它告诉对方本端的 TCP 接收缓冲区 还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。⭐️

  • 16 位校验和 (TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法 以校验TCP 报文段在传输过程中是否损坏。注意,这个校验 不仅包括 TCP 头部 ,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。

  • 16 位紧急指针 (urgent pointer):是一个正的偏移量 。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为 紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

8. TCP 滑动窗口

    滑动窗口 (Sliding window)是一种 流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。

  • 滑动窗口协议 是用来 改善吞吐量 的一种技术,即容许 发送方 在 接收任何应答之前 传送 附加的包 。接收方 告诉 发送方 在某一时刻 能送多少包 (称 窗口尺寸)。

 

    TCP 中采用 滑动窗口 来进行 传输控制 ,滑动窗口的大小意味着 接收方 还有多大的 缓冲区 可以用于接收数据。发送方 可以通过滑动窗口的大小来确定 应该发送多少字节的数据。当滑动窗口 为 0 时,发送方一般不能再发送数据报。

    滑动窗口 是 TCP 中实现诸如 ACK 确认、流量控制、拥塞控制的承载结构。

窗口 理解为 缓冲区的大小

  • 滑动窗口的大小会随着发送数据和接收数据而变化。
  • 通信的双方都有 发送缓冲区 和 接收数据的缓冲区

服务器:

  • 发送缓冲区(发送缓冲区的窗口)
  • 接收缓冲区(接收缓冲区的窗口)

客户端:

  • 发送缓冲区(发送缓冲区的窗口)
  • 接收缓冲区(接收缓冲区的窗口)

发送方的缓冲区:

  • 白色格子:空闲的空间
  • 灰色格子:数据已经被发送出去了,但是还没有被接收
  • 紫色格子:还没有发送出去的数据

接收方的缓冲区:

  • 白色格子:空闲的空间
  • 紫色格子:已经接收到的数据,还未被消化

举例 :

powershell 复制代码
# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口

1. 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3. 第三次握手
4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
5. 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6. 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7. 第12次,客户端给服务器发送了1k的数据
8. 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9. 第14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2k的数据 c:滑动窗口2k
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次回收,客户端同意了服务器端的断开请求

9. TCP 四次挥手

    四次挥手 发生在 断开连接 的时候,在程序中当调用了 close() 会使用TCP协议进行四次挥手。

    客户端和服务器端 都可以主动发起断开连接 ,谁先调用 close() 谁就是发起。

    因为在TCP连接的时候,采用 三次握手 建立的的 连接是双向 的,在断开的时候需要 双向断开。

四次挥手状态转换 :

10. TCP 通信并发

    要实现TCP通信服务器 处理 并发的任务 ,使用 多线程 或者 多进程 来解决。

(1)多进程思路:

  1. 一个父进程 ,多个子进程
  2. 父进程 负责等待并接受客户端的连接
  3. 子进程 :完成通信,接受一个客户端连接,就创建一个子进程用于通信。

⭐️ 服务器端 (server_process.c)

c 复制代码
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {  // 使用信号释放子进程资源
    while(1) { // 处理同时结束多个子进程的情况
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {

    struct sigaction act; 
    act.sa_flags = 0; 
    sigemptyset(&act.sa_mask);      // 清空临时阻塞信号掩码
    act.sa_handler = recyleChild;   // 回调函数
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 1. 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 2. 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3. 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 4. 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
            if(errno == EINTR) { // 产生中断
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 5. 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {  // 先不考虑错误的情况
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }else{
            close(cfd);
        }

    }

    // 6. 关闭文件描述符
    close(lfd);
    return 0;
}

⭐️ 客户端 (client.c)

c 复制代码
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.216.129", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);  // 加上字符串结束符

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}
  • 运行结果:
  • 中断子进程

(2)多线程实现并发服务器:

c 复制代码
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

// 把所需要参数放到一个结构体中
struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;    // 客户端信息
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];  // 可以支持128个客户端同时连接

void * working(void * arg) { 
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
            perror("read");
            exit(-1);
        }else if(len > 0) {
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {

    // 1. 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 2. 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3. 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 4. 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);

        // 5. 创建子线程,通信
        pthread_create(&pinfo->tid, NULL, working, pinfo);
        // 设置线程分离,子线程结束后,自动回收资源
        pthread_detach(pinfo->tid);
    }

    // 6. 关闭文件描述符
    close(lfd);
    return 0;
}
  • 运行结果:

11. TCP 状态转换


黑色实线:异常(可以先不看 )

红色实现:客户端

绿色虚线:服务器

  • 2MSL(Maximum Segment Lifetime)
    • 当 TCP 连接 主动关闭方 接收到 被动关闭方 发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于 TIME_WAIT 状态并持续 2MSL 时间。
    • 这样就能够让 TCP 连接 的 主动关闭方 在它发送的 ACK 丢失 的情况下 重新发送最终 的 ACK。
    • 主动关闭方 重新发送的最终 ACK 并不是因为 被动关闭方 重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为 被动关闭方 重传了它的 FIN。事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK。

主动断开 连接的一方 , 最后进出入一个 TIME_WAIT 状态, 这个状态会持续: 2msl

  • msl: 官方建议 : 2分钟, Linux中实际 是 30s

 
半关闭

    当 TCP 链接 中 A 向 B 发送 FIN 请求关闭 ,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于 半连接状态 (半开关 ),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 API 来控制实现 半连接状态:

c 复制代码
#include <sys/socket.h>
int shutdown(int sockfd, int how);
	sockfd: 需要关闭的socket的描述符
	how: 允许为shutdown操作选择以下几种方式:
		SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。(不能接收数据)
		该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
		SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发 写操作。 (不能发送数据)
		SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。

    使用 close 中止一个连接,但它只是 减少描述符的 引用计数,并不直接关闭连接,只有当 描述符的引用计数 为 0 时才关闭连接。shutdown 不考虑 描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读 或 只中止写。

注意:

  1. 如果有多个进程 共享一个套接字 ,close 每被调用一次,计数 减1 ,直到计数为 0 时,也就是所用进程都调用了 close,套接字将被释放。
  2. 在多进程中如果一个进程 调用 了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd) 将 不会影响 到其它进程。

12 端口复用

端口复用 最常用的用途是:

  • 防止 服务器重启 时之前绑定的端口还未释放
    • 当服务器主动断开连接,进入FIN_WAIT_2 或 TIME_WAIT 状态时,其端口号被占用,还未释放,不能再次启动服务器
  • 程序突然退出而系统没有释放端口
c 复制代码
##include <sys/types.h>
#include <sys/socket.h>

// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	参数:
		- sockfd : 要操作的文件描述符
		- level : 级别 - SOL_SOCKET (端口复用的级别)
		- optname : 选项的名称
			- SO_REUSEADDR  (允许重用本地地址)
			- SO_REUSEPORT  (允许重用本地端口)
		- optval : 端口复用的值(整形 / 结构体)
			- 1 : 可以复用
			- 0 : 不可以复用
		- optlen : optval参数的大小

// 端口复用,设置的时机是在服务器绑定端口之前。
setsockopt();
bind();

 

常看 网络相关信息 的命令 netstat -anp

  • 参数:
    -a 所有的 socket
    -p 显示正在使用 socket 的程序的名称
    -n 直接使用IP地址,而不通过域名服务器

注:仅供学习参考,如有不足,欢迎指正!

相关推荐
hhhjhl7 小时前
Protobuf.js 跨端三方库仓颉适配全流程
javascript·网络·udp
琥珀.8 小时前
mysql中json数组转列表查询
linux·运维·服务器
dawnsky.liu8 小时前
在 RHEL 中使用 goose 实现远程主机管理
linux·运维·ai
ITxiaobing20238 小时前
中国公司海外广告投放的 IP 地址库选型:从“查归属”到“验真实”的工程视角
网络·网络协议·tcp/ip
KKKlucifer9 小时前
运营商非结构化文档安全管控体系建设实践
大数据·网络·安全
Linux运维技术栈10 小时前
CVE-2026-91403 高危漏洞修复:Rocky Linux 9 多版本 JDK 环境清理实操
java·linux·运维
网络小江11 小时前
组网第35课:静态路由——下一跳、出接口、默认路由、浮动静态与黑洞路由
服务器·网络·默认路由·黑洞路由·浮动静态路由
acrel711 小时前
医院用电隐患治理与节能降耗及智慧能源平台应用
网络·数据库
slongzhang_11 小时前
linux 递归删除源码文件|递归删除目录下 *.js,但排除 *.min.js
linux·开发语言·javascript
叼烟扛炮11 小时前
Linux 学习笔记(三十三):线程互斥——互斥锁(Mutex)与死锁
linux·运维·服务器·笔记·学习·1024程序员节