网络编程学习——2
一、TCP连接与断开过程
1.三次握手
- 客户端发起(主动打开):客户端处于
CLOSED
状态,主动向服务端发送 SYN 报文(SYN=1
,初始序列号seq=x
),进入SYN-SENT
状态,请求建立连接。- 服务端响应(被动打开):服务端监听端口处于
LISTEN
状态,收到 SYN 报文后,回复 SYN+ACK 报文(SYN=1, ACK=1
,序列号seq=y
,确认号ack=x+1
),进入SYN-RCVD
状态,同步客户端序列号并确认收到连接请求。- 客户端确认:客户端收到 SYN+ACK 报文后,发送 ACK 报文(
ACK=1
,序列号seq=x+1
,确认号ack=y+1
),进入ESTABLISHED
状态;服务端收到 ACK 后也进入ESTABLISHED
状态。 至此,三次握手完成,双向连接建立,可开始数据传输。
2.四次挥手
- 客户端主动关闭:客户端处于
ESTABLISHED
状态,发送 FIN 报文(FIN=1
,序列号seq=u
),进入FIN-WAIT-1
状态,主动请求关闭连接。- 服务端响应(第一阶段确认):服务端收到 FIN 报文后,回复 ACK 报文(
ACK=1
,序列号seq=v
,确认号ack=u+1
),进入CLOSE-WAIT
状态,告知客户端 “已收到关闭请求”;客户端收到 ACK 后进入FIN-WAIT-2
状态,等待服务端关闭确认。- 服务端被动关闭:服务端处理完剩余数据、准备关闭时,发送 FIN+ACK 报文(
FIN=1, ACK=1
,序列号seq=w
,确认号ack=u+1
),进入LAST-ACK
状态,主动关闭连接。- 客户端最终确认:客户端收到 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;
}
测试结果
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
:永久阻塞,直到有文件描述符状态变化 - 非NULL
:tv_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;
}
运行截图