网络编程 | TCP套接字通信及编程实现经验教程

news2025/1/11 10:06:55

1、TCP基础铺垫

        TCP/IP协议簇中包含了如TCP、UDP、IP、ICMP、ARP、HTTP等通信协议。TCP协议是TCP/IP协议簇中最为常见且重要的通信方式之一,它为互联网上的数据传输提供了可靠性和连接管理。

        TCP(Transmission Control Protocol,传输控制协议)是面向连接的、可靠的、基于字节流的传输层通信协议。它主要用于在不可靠的互联网上提供可靠的数据传输。TCP被广泛应用于各种网络应用中,如Web浏览(HTTP/HTTPS)、电子邮件(SMTP、POP3、IMAP)、文件传输(FTP)等。

        TCP通信时,是一发一收的,即TCP的数据发出时,会进行一个等待确认操作,确认数据是否正常发出并被接收方接收到数据信息。

        TCP的关键特性:

(1)、面向连接:在数据传输之前,TCP需要通信双方建立一个连接。

(2)、可靠性:TCP采用了多种机制来确保数据的可靠传输,包括: 校验和 、序列号 、确认应答 、超时重传 、流量控制 、拥塞控制等技术。

(3)、全双工通信:TCP允许通信双方同时发送和接收数据,即双向通信可以在同一时间进行。

(4)、有序性:即使数据分段到达的顺序不同,TCP也能按照正确的顺序组装数据。

(5)、错误恢复:当发生错误或丢失数据时,TCP能够自动检测并尝试恢复丢失的数据。

(6)、连接终止:在数据传输完成后,TCP需要关闭连接。

(7)、多路复用:TCP支持在一个IP地址上通过不同的端口号区分多个应用程序或服务。

2、TCP关键技术梳理

(1)、TCP头部结构

(2)、三次握手

        在TCP连接建立之前,客户端和服务器之间需要进行三次握手来同步双方的序列号,并确认双方都准备好进行数据传输。

        第一次握手:客户端向服务器发送一个 SYN(同步序列编号)报文段,表示请求建立连接。客户端进入 SYN_SENT 状态。

        第二次握手:服务器收到 SYN 报文段后,回复一个 SYN-ACK(同步序列编号 + 确认)报文段,表示同意建立连接。服务器进入 SYN_RCVD 状态。

        第三次握手:客户端收到 SYN-ACK 报文段后,回复一个 ACK(确认)报文段,表示确认收到服务器的响应。客户端和服务器都进入 ESTABLISHED 状态,连接正式建立。

Client                                 Server
  |                                       |
  |  SYN (seq=0)                          |
  |-------------------------------------->|
  |                                       | SYN_RCVD
  |  <------------------------------------|
  |  SYN-ACK (ack=1, seq=0)               |
  |  ACK (seq=1, ack=1)                   |
  |-------------------------------------->|
  |                                       | ESTABLISHED
  |  ESTABLISHED                          |
  |                                       |

(3)、四次握手

        当通信结束时,客户端或服务器可以发起断开连接的请求。断开连接的过程称为四次挥手,以确保双方都能正确关闭连接并释放资源。

        第一次挥手:主动关闭方(通常是客户端)发送一个 FIN(终止)报文段,表示不再发送数据。主动关闭方进入 FIN_WAIT_1 状态。

        第二次挥手:被动关闭方(通常是服务器)收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。被动关闭方进入 CLOSE_WAIT 状态,而主动关闭方进入 FIN_WAIT_2 状态。

        第三次挥手:被动关闭方在处理完所有未完成的数据后,发送一个 FIN 报文段,表示自己也不再发送数据。被动关闭方进入 LAST_ACK 状态。

        第四次挥手:主动关闭方收到 FIN 报文段后,回复一个 ACK 报文段,表示确认收到 FIN。主动关闭方进入 TIME_WAIT 状态,等待一段时间(通常为2倍的最大报文段生命周期,即2MSL),以确保被动关闭方收到了最后的 ACK。之后,主动关闭方进入 CLOSED 状态,连接完全关闭。

Client                                 Server
  |                                       |
  |  FIN (seq=1)                          |
  |-------------------------------------->|
  |  <------------------------------------|
  |  ACK (ack=2)                          |
  |  FIN (seq=1)                          |
  |  <------------------------------------|
  |  ACK (seq=2, ack=2)                   |
  |-------------------------------------->|
  |                                       |

(4)、可靠传输

        TCP通过以下机制确保数据的可靠传输:

        序列号(Sequence Number):每个TCP报文段都有一个序列号,表示该报文段中的第一个字节在整个数据流中的位置。接收方可以根据序列号重新排序接收到的报文段,确保数据按顺序传递。

        确认应答(Acknowledgment, ACK):接收方在收到报文段后,会发送一个确认应答,告诉发送方哪些数据已经成功接收。发送方根据确认应答判断是否需要重传丢失或损坏的报文段。

        超时重传(Timeout and Retransmission):如果发送方在一定时间内没有收到确认应答,它会认为报文段可能丢失或延迟,并重新发送该报文段。TCP使用动态调整的超时机制来优化重传策略。

        流量控制(Flow Control):TCP使用滑动窗口机制来控制发送方的发送速率,确保接收方不会被过多的数据淹没。接收方会在确认应答中告知发送方当前可用的接收窗口大小,发送方根据这个信息调整自己的发送速率。

        拥塞控制(Congestion Control):TCP通过多种算法(如慢启动、拥塞避免、快速重传和快速恢复)来动态调整发送方的发送速率,避免网络拥塞。这些算法旨在在网络负载较高时减小发送速率,在网络条件改善时逐渐增加发送速率。

(5)、数据分段与重组

        TCP将应用层的数据分成多个较小的报文段(Segment),并通过IP层进行传输。每个报文段包含一个TCP头部和一部分应用层数据。接收方会根据报文段的序列号将它们重新组合成完整的数据流。

        最大报文段长度(MSS, Maximum Segment Size):为了提高传输效率并避免IP层的分片,TCP在连接建立时会协商一个合适的最大报文段长度。MSS通常由路径MTU(Maximum Transmission Unit)决定。

        分段与重组:如果应用层数据较大,TCP会将其分成多个报文段进行传输。接收方会根据序列号将这些报文段重新组合成原始的应用层数据。

(6)、连接管理

        TCP是一个面向连接的协议,这意味着在数据传输之前,必须先建立连接,传输结束后再关闭连接。TCP通过以下状态机来管理连接的生命周期:

  • LISTEN:服务器处于监听状态,等待客户端的连接请求。
  • SYN_SENT:客户端已发送 SYN 报文段,等待服务器的 SYN-ACK 响应。
  • SYN_RCVD:服务器已收到 SYN 报文段,等待客户端的 ACK 确认。
  • ESTABLISHED:连接已建立,双方可以开始传输数据。
  • FIN_WAIT_1:主动关闭方已发送 FIN 报文段,等待对方的 ACK 确认。
  • FIN_WAIT_2:主动关闭方已收到对方的 ACK,等待对方的 FIN。
  • CLOSE_WAIT:被动关闭方已收到对方的 FIN,等待应用程序关闭连接。
  • CLOSING:双方同时发送 FIN,等待对方的 ACK。
  • LAST_ACK:被动关闭方已发送 FIN,等待对方的 ACK。
  • TIME_WAIT:主动关闭方已收到对方的 FIN 和 ACK,等待2MSL后进入 CLOSED 状态。
  • CLOSED:连接已完全关闭,资源已释放。

(7)、带外数据(OOB)

        TCP支持带外数据传输,允许发送方发送紧急数据,而不必等待正常的TCP流排队。带外数据通常用于通知接收方有紧急事件发生,例如终止连接或重启服务。接收方可以通过SO_OOBINLINE选项将带外数据作为普通数据处理,或者通过 recv() 函数的 MSG_OOB 标志单独接收带外数据。

(8)、半关闭(Half-Close)

        TCP允许一方关闭连接的发送方向,而保持接收方向仍然打开。这种操作称为半关闭(Half-Close)。例如,客户端可以发送 FIN 报文段,表示不再发送数据,但仍然可以接收来自服务器的数据。服务器在收到 FIN 后,可以继续发送数据,直到它也发送 FIN 报文段,最终关闭整个连接。

3、TCP通信编程实现

3.1、TCP通信常用接口

        TCP实现网络通信的接口操作与文件操作惊人的相似,在对文件操作中,一般是先打开操作的文件描述符,然后对文件描述符配置相应的属性(如错误处理和多路复用等),设置完文件描述符的配置后,再进行对文件的读写操作,最后关闭文件描述符。而TCP网络通信时,也是先创建一个通信套接字,然后配置套接字属性及绑定、连接等操作,再进行数据的发送接收,最后关闭套接字。接下来这部分将重点介绍TCP网络通信的常用API。

        提醒:TCP通信接口API中,Windows和Linux的在使用上面存在一些区别,在下面主要介绍的是Linux平台下的API及使用。

(1)、socket
函数原型:
    #include <sys/socket.h>
    int socket(int domain, int type, int protocol);
函数功能:
    用于创建一个套接字的系统调用,它返回一个新的套接字描述符(一个非负整数),这个描述符可以用来进行后续的网络通信操作。
参数:
    domain:指定协议族(也称为地址族)
        AF_INET 或 PF_INET:IPv4 互联网协议
        AF_INET6 或 PF_INET6:IPv6 互联网协议
        AF_UNIX 或 PF_UNIX:本地通信(Unix域套接字)
        AF_ROUTE:路由套接字,用于与内核路由表交互...
    type:指定套接字类型
        SOCK_STREAM:提供面向连接、可靠的数据传输服务,使用TCP协议
        SOCK_DGRAM:提供无连接、不可靠的数据报服务,使用UDP协议
        SOCK_RAW:原始套接字,允许直接访问低层协议,如IP或ICMP
        SOCK_SEQPACKET:有序的、可靠的、双向传输的数据包服务,类似于 SOCK_STREAM,但以消息为单位
        SOCK_RDM:可靠的无连接数据报服务,保证消息按顺序到达,但不保证无重复...
    protocol:指定具体的协议
        设置为0,表示使用默认协议(根据domain和type自动选择)
        AF_INET 和 SOCK_STREAM,可以指定 IPPROTO_TCP
        AF_INET 和SOCK_DGRAM,可以指定IPPROTO_UDP
        AF_PACKET 和SOCK_RAW,可以指定具体的以太网协议,如ETH_P_IP ...
返回值:
    成功返回套接字的文件描述符(非0),失败返回-1,并设置 errno 变量来指示错误原因。
(2)、bind
函数原型:
    #include <sys/socket.h>
    int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
函数功能:
    用于将一个本地地址(IP地址和端口号)绑定到一个已创建的套接字上。
参数:
    socket:要绑定地址的套接字描述符
    address:指向包含地址信息的sockaddr 结构体的指针
        对于 IPv4 (AF_INET),使用 struct sockaddr_in
        对于 IPv6 (AF_INET6),使用 struct sockaddr_in6
        对于 Unix 域套接字 (AF_UNIX),使用 struct sockaddr_un
    address_len:指定address指向的结构体的大小,以字节为单位
        对于 sockaddr_in 通常是sizeof(struct sockaddr_in)
        对于 sockaddr_in6 通常是sizeof(struct sockaddr_in6)
返回值:
    成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(3)、listen
函数原型:
    #include <sys/socket.h>

    int listen(int socket, int backlog);
函数功能:
    用于将一个未连接的套接字转换为监听套接字,使其能够接收传入的连接请求。
参数:
    socket:已经通过 bind() 绑定到一个本地地址的套接字
    backlog:指定监听队列的最大长度,即操作系统可以为该套接字排队的最大未接受连接数
返回值:
    成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(4)、accept
函数原型: 
    #include <sys/socket.h>

    int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
函数功能:
    用于服务器端接受一个传入的连接请求。当有客户端尝试连接到一个监听中的套接字时,accept() 会创建一个新的套接字来处理这个连接,而原来的监听套接字继续等待其他连接请求。
参数:
    socket:设置为监听状态的套接字描述符。该套接字应该已经绑定到一个本地地址,并且正在监听传入的连接请求
    address:指向 struct sockaddr 结构体的指针,用于接收客户端的地址信息。如果不需要获取客户端地址,可以传递 NULL
    address_len:指向一个 socklen_t 类型变量的指针,该变量在调用时应包含 address 指向的结构体的大小。
                 函数返回时,address_len 将被更新为实际存储在 address 中的地址长度。
                 如果 address 是 NULL,则 address_len 也应该是 NULL。
返回值:
    成功时,返回一个新的文件描述符,表示与客户端的连接。这个新的套接字专门用于与特定客户端通信。
    失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因。
(5)、connect
函数原型: 
   #include <sys/socket.h>

   int connect(int socket, const struct sockaddr *address,
socklen_t address_len);
函数功能:
    用于主动发起一个到指定服务器的连接请求。将客户端套接字与远程服务器的地址和端口关联起来,从而建立一个通信通道。
参数:
    socket:已经配置好的套接字(例如,设置了协议族、类型和协议),但尚未绑定到本地地址或连接到远程地址
    address:指向 struct sockaddr 结构体的指针,该结构体包含要连接的远程服务器的地址信息
        对于 IPv4 (AF_INET),通常使用 struct sockaddr_in
        对于 IPv6 (AF_INET6),通常使用 struct sockaddr_in6
        对于 Unix 域套接字 (AF_UNIX),通常使用 struct sockaddr_un
    address_len:指定address指向的结构体的大小,以字节为单位
        对于 sockaddr_in 通常是sizeof(struct sockaddr_in)
        对于 sockaddr_in6 通常是sizeof(struct sockaddr_in6)
返回值:
    成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。
(6)、send
函数原型: 
    #include <sys/socket.h>

    ssize_t send(int socket, const void *buffer, size_t length, int flags);
函数功能:
    用于通过已连接的套接字发送数据。它通常用于面向连接的协议(如TCP),但也可以用于无连接的协议(如UDP)。
参数:
    socket:已经连接的套接字描述符
    buffer:指向要发送的数据缓冲区的指针。该缓冲区包含要传输的字节数据
    length:要发送的数据长度,以字节为单位。如果缓冲区中的数据长度小于 len,则只发送缓冲区中实际存在的数据    
    flags:控制 send() 行为的标志
        0:默认行为,没有特殊选项
        MSG_OOB:发送带外数据(out-of-band data),适用于支持带外数据的协议(如TCP)
        MSG_DONTROUTE:跳过路由表查找,直接将数据发送到目标地址。通常用于诊断工具或特定网络配置
        MSG_DONTWAIT:使 send() 调用非阻塞。如果套接字是阻塞模式,此标志会使 send() 在无法立即发送所有数据时返回,而不是等待。如果套接字已经是非阻塞模式,此标志通常不会产生额外效果
        MSG_NOSIGNAL:防止 SIGPIPE 信号在尝试向已关闭的连接写入时生成。这对于避免程序意外终止非常有用
返回值:
    成功时,返回实际发送的字节数,可能小于length。
    失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因
(7)、recv
函数原型: 
    #include <sys/socket.h>

    
    ssize_t recv(int socket, void *buffer, size_t length, int flags);
函数功能:
    用于从已连接的套接字接收数据。它通常用于面向连接的协议(如TCP),但也可以用于无连接的协议(如UDP)。
参数:
    socket:已经连接的套接字描述符。
    buffer:指向接收数据缓冲区的指针。该缓冲区将存储从套接字接收到的数据。
    length:指定缓冲区的最大长度,以字节为单位。
    flags:控制 recv() 行为的标志
        0:默认行为,没有特殊选项
        MSG_OOB:接收带外数据(out-of-band data),适用于支持带外数据的协议(如TCP)
        MSG_PEEK:窥视模式,数据被读取到缓冲区但不从输入队列中移除。下次调用 recv() 时仍然可以读取这些数据
        MSG_WAITALL:等待直到接收到请求的所有数据(len 个字节)。如果设置了这个标志,recv() 可能在接收到部分数据后仍然阻塞,直到接收到所有数据或发生错误
        MSG_DONTWAIT:使 recv() 调用非阻塞。如果套接字是阻塞模式,此标志会使 recv() 在无法立即读取数据时返回,而不是等待。如果套接字已经是非阻塞模式,此标志通常不会产生额外效果
返回值:
    成功时,返回实际接收到的字节数。如果返回值为 0,表示对端已经关闭了连接。
    失败时,返回 -1,并且会设置 errno 变量来指示具体的错误原因。
(8)、close
函数原型: 
    #include <unistd.h>

    int close(int fildes);
函数功能:
    用于关闭一个打开的文件描述符,包括普通文件、设备、管道以及套接字等。
    关闭文件描述符后,操作系统将释放与该描述符相关的资源,并使其可以被重新分配给其他文件或套接字。
参数:
    fildes:要关闭的文件描述符。这个描述符可以是通过 open()、socket()、pipe() 等函数创建的。
返回值:
    成功返回0,失败返回 -1,并设置 errno 变量来指示错误原因。

3.2、TCP通信代码

(1)、客户端代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

#define SERVER_IP "127.0.0.1"  // 服务器IP地址
#define SERVER_PORT 8080       // 服务器端口
#define LOCAL_IP "0.0.0.0"     // 本地IP地址 (INADDR_ANY 表示任意可用接口)
#define LOCAL_PORT 6666       // 本地端口 (0 表示由操作系统选择)
#define BUFFER_SIZE 1024       // 缓冲区大小

void error_exit(const char *msg) 
{
    perror(msg);
    exit(EXIT_FAILURE);
}

void handle_receive(int client_sockfd) 
{
    char buffer[BUFFER_SIZE];
    ssize_t bytes_received;

    while (1) 
    {
        bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE - 1, 0);
        if (bytes_received < 0) 
        {
            perror("Receive failed");
            break;
        } else if (bytes_received == 0) 
        {
            printf("Server closed the connection\n");
            break;
        } else {
            buffer[bytes_received] = '\0'; // 确保字符串以null结尾
            printf("Received %zd bytes: %s\n", bytes_received, buffer);
        }
    }

    close(client_sockfd); // 关闭客户端套接字
    printf("Client socket closed in receive process\n");
    exit(EXIT_SUCCESS);   // 子进程退出
}

int main(int argc, const char **argv) 
{
    int client_sockfd;
    struct sockaddr_in server_addr, local_addr;
    pid_t child_pid;

    // 1、创建一个TCP套接字
    client_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (client_sockfd < 0) 
    {
        error_exit("Socket creation failed");
    }
    printf("Socket created successfully\n");

    // 配置本地地址结构
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(LOCAL_PORT); // 设置本地端口
    if (inet_pton(AF_INET, LOCAL_IP, &local_addr.sin_addr) <= 0) 
    {
        error_exit("Invalid local address/ Address not supported");
    }

    // 2、绑定本地地址
    if (bind(client_sockfd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) 
    {
        error_exit("Bind failed");
    }
    printf("Bound to local address %s:%d\n", LOCAL_IP, LOCAL_PORT);

    // 配置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) 
    {
        error_exit("Invalid server address/ Address not supported");
    }

    // 3、连接到服务器
    if (connect(client_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) 
    {
        error_exit("Connection failed");
    }
    printf("Connected to server at %s:%d\n", SERVER_IP, SERVER_PORT);

    // 4、创建子进程处理接收数据
    child_pid = fork();
    if (child_pid < 0) 
    {
        error_exit("Fork failed");
    } else if (child_pid == 0) 
    {
        // 子进程:关闭监听套接字,处理接收数据
        handle_receive(client_sockfd);
    } else 
    {
        // 父进程:继续发送数据
        // 5、发送数据
        const char *message = "Hello, Server!";
        ssize_t bytes_sent = send(client_sockfd, message, strlen(message), 0);
        if (bytes_sent < 0) 
        {
            error_exit("Send failed");
        }
        printf("Sent %zd bytes: %s\n", bytes_sent, message);
        printf("Enter message to send (or type 'exit' to quit): \n");
        // 父进程可以继续发送更多数据
        while (1) 
        {
            char buffer[BUFFER_SIZE] = {0};
            fgets(buffer, BUFFER_SIZE, stdin);
            buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行符

            if (strcmp(buffer, "exit") == 0) 
            {
                break;
            }

            bytes_sent = send(client_sockfd, buffer, strlen(buffer), 0);
            if (bytes_sent < 0) 
            {
                error_exit("Send failed");
            }
            printf("Sent %zd bytes: %s\n", bytes_sent, buffer);
        }
        
        // 6、关闭套接字
        close(client_sockfd);
        printf("Socket closed\n");

        // 等待子进程结束,避免僵尸进程
        waitpid(child_pid, NULL, 0);
    }

    return 0;
}
(2)、服务器代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>

#define SERVER_IP "127.0.0.1"   // 服务器IP地址,如果为 0.0.0.0则是监听所有的IP
#define SERVER_PORT 8080        // 服务器端口
#define BUFFER_SIZE 1024        // 缓冲区大小
#define BACKLOG 5               // 监听队列的最大长度

void error_exit(const char *msg) 
{
    perror(msg);
    exit(EXIT_FAILURE);
}

void handle_client(int client_sockfd) 
{
    char buffer[BUFFER_SIZE];
    ssize_t bytes_received;

    while ((bytes_received = recv(client_sockfd, buffer, BUFFER_SIZE - 1, 0)) > 0) 
    {
        buffer[bytes_received] = '\0'; // 确保字符串以null结尾
        printf("Received %zd bytes from client: %s\n", bytes_received, buffer);

        // 回显接收到的数据
        if (send(client_sockfd, buffer, bytes_received, 0) < 0) 
        {
            perror("Send failed");
            break;
        }
    }

    if (bytes_received == 0) 
    {
        printf("Client closed the connection\n");
    } else if (bytes_received < 0) 
    {
        perror("Receive failed");
    }

    close(client_sockfd); // 关闭客户端套接字
    printf("Client socket closed\n");
}

int main(int argc, const char **argv) 
{
    int server_sockfd, client_sockfd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pid_t child_pid;

    //1、创建一个TCP套接字
    server_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (server_sockfd < 0) 
    {
        error_exit("Socket creation failed");
    }
    printf("Socket created successfully\n");

    // 配置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) 
    {
        error_exit("Invalid server address/ Address not supported");
    }

    //2、绑定套接字到指定的地址和端口
    if (bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) 
    {
        error_exit("Bind failed");
    }
    printf("Bind completed\n");

    //3、开始监听连接请求
    if (listen(server_sockfd, BACKLOG) < 0)  // backlog 设置为 5
    {
        error_exit("Listen failed");
    }
    printf("Server listening on port %d\n", SERVER_PORT);

    //4、接受并处理客户端连接
    while (1) 
    {
        client_addr_len = sizeof(client_addr);
        client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_addr_len);
        if (client_sockfd < 0) 
        {
            perror("Accept failed");
            continue;
        }

        printf("Accepted connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

        // 创建子进程处理客户端连接
        child_pid = fork();
        if (child_pid < 0) 
        {
            perror("Fork failed");
            close(client_sockfd);
            continue;
        } else if (child_pid == 0) 
        {
            // 子进程:关闭监听套接字,处理客户端连接
            close(server_sockfd);
            handle_client(client_sockfd);
            exit(EXIT_SUCCESS);
        } else {
            // 父进程:关闭客户端套接字,继续监听新的连接
            close(client_sockfd);
            // 等待子进程结束,避免僵尸进程
            waitpid(-1, NULL, WNOHANG);
        }
    }

    //5、关闭监听套接字
    close(server_sockfd);
    printf("Server socket closed\n");

    return 0;
}

        如下图所示的是TCP的客户端与服务器的API调用过程的核心梳理总结。

4、TCP通信展示

        上述TCP客户端与服务器代码编译运行后,实现的效果如下所示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2256950.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3组件间传值

definProps方式 子组件&#xff1a;assignSuppliers.vue const propdefineProps({fid:String}); 父组件&#xff1a;index.vue <!-- 供应商分配 --><n-drawerwidth"800"v-model:visible"drawerSupplierConfig.visible":title"drawerSuppli…

《网络安全编程基础》之Socket编程

我的代码 server.c // server.cpp : Defines the entry point for the console application. //#include "stdafx.h" #include <Winsock2.h> #pragma comment(lib,"ws2_32.lib") //添加静态链接库文件 void main(int argc,char* argv[]) {WSADATA …

不只是请求和响应:使用Fiddler解读Cookie与状态码全指南(下)

欢迎浏览高耳机的博客 希望我们彼此都有更好的收获 感谢三连支持! 不只是请求和响应&#xff1a;使用Fiddler抓包HTTP协议全指南(上)_fiddler 获取响应脚本-CSDN博客https://blog.csdn.net/Chunfeng6yugan/article/details/144005872?spm1001.2014.3001.5501 不只是请求和响…

Linx下自动化之路:Redis安装包一键安装脚本实现无网极速部署并注册成服务

目录 简介 安装包下载 安装脚本 服务常用命令 简介 通过一键安装脚本实现 Redis 安装包的无网极速部署&#xff0c;并将其成功注册为系统服务&#xff0c;开机自启。 安装包下载 redis-7.0.8.tar.gzhttp://download.redis.io/releases/redis-7.0.8.tar.gz 安装脚本 修…

第3章.垃圾收集器与内存分配策略

概述 对象已死 引用计数法 可达性分析算法 再谈引用 生存还是死亡 回收方法区 垃圾收集算法 分代收集理论 3种垃圾收集算法 HotSpot的算法细节实现 根节点枚举 安全点 安全区域 记忆集与卡表 写屏障 并发的可达性分析 误消亡问题 经典垃圾收集器 概述 简单的一些GC CMS G1 低延…

Python 类的设计(以植物大战僵尸为例)

关于类的设计——以植物大战僵尸为例 一、设计类需满足的三要素1. 类名2. 属性和方法 二、以植物大战僵尸的为例的类的设计1. 尝试分类2. 创建对象调用类的属性和方法*【代码二】*3. 僵尸的继承 三、代码实现 一、设计类需满足的三要素 1. 类名 类名&#xff1a;某类事物的名…

如何使用WinCC DataMonitor基于Web发布浏览Excel报表文档

本文介绍使用 WinCC DataMonitor 的 "Excel Workbooks" 功能&#xff0c;通过 Excel 表格显示 WinCC 项目的过程值、归档变量值和报警归档消息。并可以通过 Web 发布浏览访问数据 1&#xff0e;WinCC DataMonitor是什么 ? DataMonitor 是 SIMATIC WinCC 工厂智能中…

【Java】—— 图书管理系统

基于往期学习的类和对象、继承、多态、抽象类和接口来完成一个控制台版本的 “图书管理系统” 在控制台界面中实现用户与程序交互 任务目标&#xff1a; 1、系统中能够表示多本图书的信息 2、提供两种用户&#xff08;普通用户&#xff0c;管理员&#xff09; 3、普通用户…

记录ubuntu22.04重启以后无法获取IP地址的问题处理方案

现象描述&#xff1a;我的虚拟机网络设置为桥接模式&#xff0c;输入ifconfig只显示127.0.0.1&#xff0c;不能连上外网。&#xff0c;且无法上网&#xff0c;用ifconfig只有如下显示&#xff1a; 1、sudo -i切换为root用户 2、输入dhclient -v 再输入ifconfig就可以看到多了…

异步操作,promise、axios

一、异步操作&#xff08;异步编程&#xff09;、同步操作 异步操作是指在编程中&#xff0c;某个任务的执行不会立即完成&#xff0c;同时不会阻塞后续代码的执行。在异步操作中&#xff0c;程序可以继续运行&#xff0c;并在异步任务完成时得到通知并处理结果。这与同步操作…

第一性原理构造医疗信创域高质量发展路径应用探析

门诊电子病历录入 摘要&#xff1a; 主要介绍了第一性原理在医疗系统开发中的应用及其重要性。阐述了第一性原理的概念及发展历程&#xff0c;并指出其在各个领域的重要性和应用价值。详细分析了第一性原理在医疗系统开发中的具体影响&#xff0c;包括对医院管理和互联网医疗的…

MySQL8下载安装教程

前言 1.个人经验&#xff0c;仅供参考&#xff01;&#xff01;&#xff01; 2.如果之前有下载过MySQL&#xff0c;请检查是否有删除干净&#xff0c;在控制面板删除最好 下载网址&#xff1a;MySQL :: MySQL Community Downloads 下载步骤 进入网址选择要下载的 下一步网…

算法日记 45 day 图论(并查集基础)

并查集解决什么问题 并查集常用来解决连通性问题。 大白话就是当我们需要判断两个元素是否在同一个集合里的时候&#xff0c;我们就要想到用并查集。 原理 既然是查找是否在同一个集合中&#xff0c;那么这个集合是怎么构建的呢&#xff1f;用一维数组来表示一个有向图&…

PTA DS 6-4 带头结点的链队列的基本操作 (C补全函数)

6-4 带头结点的链队列的基本操作 分数 10 全屏浏览 切换布局 作者 黄复贤 单位 菏泽学院 实现链队列的入队列及出队列操作。 函数接口定义&#xff1a; Status QueueInsert(LinkQueue *Q,ElemType e)&#xff1b; Status QueueDelete(LinkQueue *Q,ElemType *e)&#x…

Windows 系统没有网络链接常见原因以及解决方案

在使用 Windows 电脑时&#xff0c;有时会遇到电脑显示已连接网络&#xff0c;但却无法访问 Internet 的情况&#xff0c;这可能是由多种原因导致的。以下简鹿办公将详细介绍一些常见原因及对应的解决方案。 一、网络连接问题 原因 路由器故障&#xff1a;路由器长时间运行可…

lnmp+discuz论坛 附实验:搭建discuz论坛

Inmpdiscuz论坛 Inmp: t: linux操作系统 nr: nginx前端页面 me: mysql数据库 账号密码&#xff0c;等等都是保存在这个数据库里面 p: php——nginx擅长处理的是静态页面&#xff0c;页面登录账户&#xff0c;需要请求到数据库&#xff0c;通过php把动态请求转发到数据库 n…

杨振宁大学物理视频中黄色的字,c#写程序去掉

先看一下效果&#xff1a;&#xff08;还有改进的余地&#xff09; 我的方法是笨方法&#xff0c;也比较刻板。 1&#xff0c;首先想到&#xff0c;把屏幕打印下来。c#提供了这样一个函数&#xff1a; Bitmap bmp new Bitmap(640, 480, PixelFormat.Format32bppArgb); // 创…

Openlayers基础知识回顾(五)

1、GeoJSON数据的加载 GeoJSON是一种基于JSON的地理空间数据交换格式&#xff0c;它定义了几种类型JSON对象以及它们组合在一起的方法&#xff0c;以表示有关地理要素、属性和它们的空间范围的数据 2、GeoJSON转化为ol要素 new ol.format.GeoJSON().readFeatures() 一、canv…

VTK知识学习(21)- 数据的读写

1、前言 对于应用程序而言&#xff0c;都需要处理特定的数据&#xff0c;VTK应用程序也不例外。 VTK应用程序所需的数据可以通过两种途径获取: 第一种是生成模型&#xff0c;然后处理这些模型数据(如由类 vtkCylinderSource 生成的多边形数据); 第二种是从外部存储介质里导…

javaWeb之过滤器(Filter)

目录 前言 过滤器概述 什么是过滤器 过滤器详细 过滤器的生命周期 过滤器的应用 创建一个简单的Filter类步骤 注意&#xff1a;指定拦截路径&#xff0c;我们有两种方式 实例 前言 本篇博客的核心 知道过滤器的整个拦截过程知道如何指定拦截路径知道过滤器的生命周期…