网络编程学习——2

一、TCP连接与断开过程

1.三次握手

alt

  1. 客户端发起(主动打开):客户端处于 CLOSED 状态,主动向服务端发送 SYN 报文SYN=1,初始序列号 seq=x ),进入 SYN-SENT 状态,请求建立连接。
  2. 服务端响应(被动打开):服务端监听端口处于 LISTEN 状态,收到 SYN 报文后,回复 SYN+ACK 报文SYN=1, ACK=1,序列号 seq=y,确认号 ack=x+1 ),进入 SYN-RCVD 状态,同步客户端序列号并确认收到连接请求。
  3. 客户端确认:客户端收到 SYN+ACK 报文后,发送 ACK 报文ACK=1,序列号 seq=x+1,确认号 ack=y+1 ),进入 ESTABLISHED 状态;服务端收到 ACK 后也进入 ESTABLISHED 状态。 至此,三次握手完成,双向连接建立,可开始数据传输。

2.四次挥手

alt

  1. 客户端主动关闭:客户端处于 ESTABLISHED 状态,发送 FIN 报文FIN=1,序列号 seq=u ),进入 FIN-WAIT-1 状态,主动请求关闭连接。
  2. 服务端响应(第一阶段确认):服务端收到 FIN 报文后,回复 ACK 报文ACK=1,序列号 seq=v,确认号 ack=u+1 ),进入 CLOSE-WAIT 状态,告知客户端 “已收到关闭请求”;客户端收到 ACK 后进入 FIN-WAIT-2 状态,等待服务端关闭确认。
  3. 服务端被动关闭:服务端处理完剩余数据、准备关闭时,发送 FIN+ACK 报文FIN=1, ACK=1,序列号 seq=w,确认号 ack=u+1 ),进入 LAST-ACK 状态,主动关闭连接。
  4. 客户端最终确认:客户端收到 FIN+ACK 报文后,回复 ACK 报文ACK=1,序列号 seq=u+1,确认号 ack=w+1 ),进入 TIME-WAIT 状态(等待 2MSL 确保报文可靠传递);服务端收到 ACK 后进入 CLOSED 状态;客户端等待超时后也进入 CLOSED 状态。 至此,四次挥手完成,连接彻底断开,释放资源。

3.关键设计意义:

  • 三次握手通过 “同步序列号 + 双向确认”,避免历史残留连接干扰,确保连接可靠;
  • 四次挥手通过 “两次 FIN+ACK 交互”,处理双向数据传输的收尾需求,保证双方数据完整关闭。

二、并发服务器

1.进程并发(流程)

1.创建套接字

2.绑定套接字

3.监听套接字

4.并发通信

while(1){

​ 4.1被动等待连接

​ 4.2创建一个进程 -> 子进程处理请求

}

注意:当 recv 返回 0 时,说明客户端已经退出

服务器就应该结束子进程,并且父进程回收子进程的资源

server.c

/*===============================================
*   文件名称:server_P.c
*   创 建 者:青木莲华
*   创建日期:2025年08月14日
*   描    述:并发——Server
================================================*/
#include "socket_head.h"
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
//回收进程函数
void handler_child(int arg)
{
       wait(NULL);
}


int main(int argc, char *argv[])
{ 
    
    char buf[1024] = {0};           //缓冲区
    int ret = -1;                   //recv返回值判断条件
    signal(SIGCHLD,&handler_child);  //处理子进程信号
    //1.创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    //2.绑定套接字
    
    //2.1 初始化结构体等参数
    struct sockaddr_in addr;
    //设置IP类型
    addr.sin_family = AF_INET;
    //设置端口
    addr.sin_port = htons(5555);
    //设置ip
    addr.sin_addr.s_addr = inet_addr("192.168.6.174");
    //2.2 使用bind函数绑定
    if(-1 == bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        perror("bind");
        close(sockfd);
        return -1;
    }
    //3.监听套接字
    if(-1 == listen(sockfd,5))
    {
        perror("listen");
        close(sockfd);
        return -1;
    } 
    
    printf("Server is waiting for connection\n");
    socklen_t addrlen = sizeof(addr);
    //4.并发
    while(1)
    {
        int connfd = accept(sockfd ,(struct sockaddr *)&addr,&addrlen);
        if(-1 == connfd)
        {
            perror("accept");
            close(sockfd);
            return -1;
        }
        pid_t pid = fork();
        if(-1 == pid)
        {
            perror("fork");
            close(connfd);
            exit(0);
        }
        else if(0 == pid)       //子进程
        {
            printf("A client connect. IP :%s:%d\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
            while(1)
            {
                ret = recv(connfd,buf,sizeof(buf),0);
                if(-1 == ret)
                {
                    perror("recv");
                    close(connfd);
                    exit(0);
                }
                else if(0 == ret)
                {
                    printf("Client <%s:%d> has disconnected\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
                    close(connfd);
                    exit(0);
                }
                puts(buf);
            }
        }
    }
    return 0;
} 

测试结果

alt

2.线程并发

1.创建套接字

2.绑定套接字

3.监听套接字

while(1){

​ 4.被动等待连接

​ 5.创建线程

​ 6.线程分离(自动回收)

}

server

/*===============================================
*   文件名称:server_thread.c
*   创 建 者:青木莲华
*   创建日期:2025年08月14日
*   描    述:并发——Server 线程实现
================================================*/
#include "socket_head.h"
#include <sys/types.h>
#include <pthread.h>
//线程函数(用于多线程通信)
void *handler_thread(void *arg)
{
    
    char buf[1024] = {0};           //缓冲区
    int ret = -1;                   //recv返回值判断条件
    int connfd = (int ) arg;        //已经建立连接的套接字文件描述符
    while(1)
    {
        ret = recv(connfd,buf,sizeof(buf),0);
        if(-1 == ret)
        {
            printf("Connection of %d has recv error!\n",connfd);
            continue;
        }
        if(0 == ret)
        {
            printf("Connection of %d has disconnected!\n",connfd);
            break;
        }
        puts(buf);
    }
    return NULL;
}



int main(int argc, char *argv[])
{ 
    
    //1.创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    //2.绑定套接字
    
    //2.1 初始化结构体等参数
    struct sockaddr_in addr;
    //设置IP类型
    addr.sin_family = AF_INET;
    //设置端口
    addr.sin_port = htons(5555);
    //设置ip
    addr.sin_addr.s_addr = inet_addr("192.168.6.174");
    //2.2 使用bind函数绑定
    if(-1 == bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        perror("bind");
        close(sockfd);
        return -1;
    }
    //3.监听套接字
    if(-1 == listen(sockfd,5))
    {
        perror("listen");
        close(sockfd);
        return -1;
    } 
    
    printf("Server is waiting for connection\n");
    socklen_t addrlen = sizeof(addr);
    //4.并发
    while(1)
    {
        int connfd = accept(sockfd ,(struct sockaddr *)&addr,&addrlen);
        if(-1 == connfd)
        {
            perror("accept");
            close(sockfd);
            return -1;
        }

        printf("Client of %d has connected , IP : <%s:%d> \n",connfd,inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
        pthread_t thread;
        if(0 != pthread_create(&thread,NULL,handler_thread,(void *)connfd))
        {
            printf("<%s:%d> this client's pthread can't create!\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
        }
        else
            pthread_detach(thread);
    }
    close(sockfd);
    return 0;
} 

3.速度:

进程创建和消亡开销比较大,速度慢(连接/断开连接)

线程通信简单

进程稳定性更高

三、IO模型

阻塞 简单

非阻塞 需要轮询

信号 触发机制

IO多路复用 select < poll < epoll

IO多路复用

1. 位图(核心机制)

在 IO 多路复用中,位图是一种高效管理文件描述符(FD)状态的数据结构,其核心原理是通过二进制位(bit)的状态来标记

文件描述符的 “是否被关注” 或 “是否就绪”。

每个二进制位(bit)对应一个文件描述符(FD)。

位的值(0 或 1)表示状态

​ 1——表示该文件描述符被 “关注”(如需要监听其可读 / 可写事件),或已 “就绪”(如数据已到达)。

​ 0——表示该文件描述符未被关注,或未就绪。

核心原理:高效的位运算操作

​ 位图的高效性源于位运算的快速性(CPU 原生支持,单条指令即可操作多个位)

fd_set 的操作函数本质都是对位图的位运算

操作函数 位运算逻辑(示例) 作用
FD_ZERO(set) 将位图所有位设为 0 清空集合,初始化监听列表
FD_SET(fd,set) 将第fd位设为 1 添 FD 到监听列表
FD_CLR(fd,set) 将第fd位设为 0 从监听列表移除 FD
FD_ISSET(fd,set) 检查第fd位是否为 1 判断 FD 是否就绪

FD_ISSET函数返回值:0 未就绪 , 非0 就绪

2.select函数

函数功能

select函数是一种 I/O 多路复用机制,用于检测多个文件描述符(如套接字、管道等)的状态,从而实现同时处理多个 I/O

操作,避免程序阻塞在单一的 I/O 操作上,提升程序的并发处理能力。

函数头文件 + 函数原型

#include <sys/select.h> 
#include <sys/time.h> 
#include <sys/types.h> 
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval 
*timeout);

参数

参数名 类型 说明
nfds int 需要检查的文件描述符集合中,最大文件描述符的值加 1(用于限定检查范围)
readfds fd_set * 指向可读文件描述符集合的指针,用于检测哪些文件描述符可读
writefds fd_set * 指向可写文件描述符集合的指针,用于检测哪些文件描述符可写
exceptfds fd_set * 指向异常文件描述符集合的指针,用于检测哪些文件描述符发生异常
timeout struct timeval * 超时时间设置: - NULL:永久阻塞,直到有文件描述符状态变化 - 非 NULLtv_sec(秒)+ tv_usec(微秒),超时后返回

返回值

返回值 含义说明
> 0 成功,返回状态发生变化的文件描述符总数(可读 / 可写 / 异常)
= 0 超时,在 timeout 规定时间内,没有文件描述符状态发生变化
= -1 失败,错误原因可通过 errno 获取(如 EBADF 无效描述符、EINTR 被信号中断等)

四、作业

使用select写一个tcp并发服务器

1.创建套接字

2.绑定套接字

3.监听

4.把sockfd加入位图

while(1)

{

​ 5.select

​ 6.判断是不是sockfd准备就绪

​ 7.如果是sockfd则 accept ,并更新位图

​ 8.如果不是sockfd:recv,客户端退出,更新位图,删除断开的文件描述符

}

server代码

/*===============================================
*   文件名称:server_select.c
*   创 建 者:青木莲华
*   创建日期:2025年08月14日
*   描    述:TCP_server 并发——基于Select多路复用
================================================*/
#include "socket_head.h"

//获得最大fd
int get_nfds(int conn[1024],int nfds)
{

    int max_fd = -1;
    // 从后往前遍历,找到第一个值为1的下标(即最大已连接FD)
    for (int i = nfds; i >= 0; i--) {
        if (conn[i] == 1) {
            max_fd = i + 1;
            break;
        }
    }
    return max_fd;
}


int main(int argc, char *argv[])
{ 
   
    int conn[1024] = {0};   //标识哪些fd建立连接,用于管理nfds
    char buf[1024] = {0};   //缓冲区
    int ret = -1;           //recv接收参数
    //1.创建套接字
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    //2.初始化结构体并绑定套接字
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(6666);
    addr.sin_addr.s_addr = inet_addr("192.168.18.220");
    
    if(-1 == bind(sockfd,(struct sockaddr *)&addr,sizeof(addr)))
    {
        perror("bind");
        return -1;
    }
    //3.监听
    if(-1 == listen(sockfd,5))
    {
        perror("listen");
        return -1;
    }
    printf("Server is waiting for connect!\n");
    //4.初始化fd_set 位图,并为服务器套接字关注
    fd_set fds,temp;
    int nfds = sockfd + 1;
    FD_ZERO(&fds);          //初始化清空
    FD_SET(sockfd,&fds);    //将服务器套接字关注
    conn[sockfd] = 1;       
    temp = fds; 
    socklen_t addlen = sizeof(addr);

    while(1)
    {
        //1.轮询调用select 查看是否有文件描述符触发
        if(-1 == select(nfds,&temp,NULL,NULL,NULL))
        {
            perror("select");
            break; 
        }
        //2.判断是否有连接
        if(0 != FD_ISSET(sockfd,&temp))
        {
            //有新连接
            int connfd = accept(sockfd,(struct sockaddr *)&addr,&addlen);
            if(-1 == connfd)
            {
                perror("accept");
                break;
            }
            printf("Client<%s:%d> is connect!\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
            //更新位图及各项参数
            FD_SET(connfd,&fds);
            conn[connfd] = 1;
            nfds = get_nfds(conn,nfds);
            printf("connfd: %d\n",connfd);
            printf("nfds : %d\n",nfds);
        }
        for(int i = 4 ; i <= nfds ; i++)
        {
            if(0 != FD_ISSET(i,&temp))
            {
                ret = recv(i,buf,sizeof(buf),0);
                printf("ret : %d\n",ret);
                if(-1 == ret)
                {
                    perror("recv");
                    conn[i] = 0;
                    FD_CLR(i,&fds);    //将当前fd 置0
                    close(i);
                    continue;
                }
                else if(0 == ret)
                {
                    printf("An Client has disconnected...\n");
                    conn[i] = 0;
                    FD_CLR(i,&fds);
                    close(i);
                }
                else
                {
                    puts(buf);
                }
            }
        }
    temp = fds;
    }
    close(sockfd);
    return 0;
} 

运行截图

alt

alt