TCP‑多进程并发服务器原理
思路:
- 父进程循环
accept(),不停等待新客户端连接; - 一旦有客户端接入,
fork()创建子进程; - 子进程负责和该客户端收发数据;
- 父进程关闭本次连接的
connfd,立刻回到 accept,继续等待下一个客户端; - 子进程用完连接后关闭
connfd,退出; - 增加信号处理
SIGCHLD,回收僵尸子进程。
优点
-
代码逻辑简单、上手容易 每个客户端一条独立进程,进程之间内存完全隔离;你只需要编写单客户端收发逻辑,不需要处理 IO 多路复用、就绪事件、非阻塞、EAGAIN 等复杂坑。
-
天然阻塞 IO,收发互不干扰 子进程里
read(connfd)阻塞,完全不会影响父进程accept;各个客户端之间收发完全独立,一个客户端阻塞不会拖垮其他客户端。 -
稳定性高、故障隔离好 某一个客户端发生异常、崩溃,只会销毁对应的子进程;不会影响父进程和其他客户端连接。
-
不用纠结 select 时序陷阱 不会遇到你前面踩过的:select 休眠期间键盘输入丢失、
read(0)阻塞卡死 socket 读取一类问题。 如果想要双向聊天,子进程内部再 fork 一次,一条收一条发,轻松实现双向自由发送。 -
阻塞 IO 编程难度最低 read/write 默认阻塞,不用设置
O_NONBLOCK,不用处理EAGAIN,缓冲区由操作系统 TCP 协议栈自动管理。
缺点
-
资源开销大 每接入 1 个客户端就要创建 1 个进程。进程拥有独立地址空间、页表、文件描述符表。 客户端数量非常多(上千)时,CPU、内存开销陡增;进程切换开销变大,服务器性能下降。
-
进程间通信麻烦 各个客户端属于不同进程,进程之间不能直接共享数据。 如果需要所有客户端群发消息(聊天室),必须使用 IPC:共享内存、管道、消息队列,实现起来复杂。
-
僵尸进程隐患 子进程退出会发送
SIGCHLD信号,父进程如果没有waitpid回收,就会产生大量僵尸进程,耗尽 PID 号。 -
进程创建慢
fork()创建进程开销大于pthread创建线程;高并发短连接场景,频繁创建销毁进程会造成性能损耗。
练习:多进程并发服务器,实现服务器回发客户端传送来的消息
服务器代码:
-
程序启动,注册信号处理函数
signal(SIGCHLD,sighandler);提前告诉内核:以后子进程退出,自动调用 wait 回收,杜绝僵尸进程。 -
创建 TCP 监听套接字 socket () 操作系统返回一个文件描述符
fd,就是监听套接字。 -
填充服务器地址结构体 设置服务器 IP:
192.168.31.94,端口:50000; 注意:IP、端口必须转成网络大端序! -
bind () 绑定 把套接字 fd 和 IP + 端口绑定在一起。客户端就可以通过
192.168.31.94:50000连接服务器。 -
listen () 开启监听 套接字从主动套接字 → 被动监听套接字,开始等待客户端发起 TCP 连接。
-
进入 while (1) 循环,循环接收客户端
accept(fd,...)阻塞,没有客户端连接时程序停在这里休眠。- 一旦有客户端发起 TCP 三次握手连接成功:
- accept 返回一个全新通信文件描述符 connfd(专门用来和这个客户端收发数据)
- 获取并打印客户端的 IP 地址、端口号
-
fork () 创建子进程实现并发
- 父进程 (pid>0):不做任何通信工作,直接回到循环开头,再次调用 accept,继续等待下一个客户端连接;
- 子进程 (pid=0):接管 connfd,进入收发循环,和当前客户端单独通信。
-
子进程通信循环
read(connfd,buf):阻塞读取客户端发来的数据- 如果读到
quit:关闭 connfd,子进程 exit 退出;父进程收到 SIGCHLD 信号,调用 wait 回收子进程; - 没有收到 quit:打印收到的消息;拼接回复字符串
server+xxx;write 把数据发回客户端。 - 循环,等待客户端下一条消息。
#include<stdio.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>/**
- @brief SIGCHLD信号处理函数,回收僵尸子进程
- @param sig 触发的信号编号(SIGCHLD)
- 当子进程退出时,内核会给父进程发送SIGCHLD信号
- 父进程调用wait(),等待子进程结束,释放子进程资源,避免产生僵尸进程
*/
void sighandler(int sig)
{
wait(NULL);
}
int main(int argc, const char *argv[])
{
// 注册信号捕捉函数:子进程退出自动调用sighandler回收资源
signal(SIGCHLD,sighandler);// 创建流式套接字,IPv4,TCP协议 // AF_INET:使用IPv4地址族 // SOCK_STREAM:流式套接字,对应TCP // 0:自动选择对应协议(TCP) int fd = socket(AF_INET,SOCK_STREAM,0); if(fd < 0) { // 创建套接字失败打印错误信息 perror("socket fail"); return -1; } printf("fd = %d\n",fd); // 定义服务器地址结构体,存放服务器的IP和端口 struct sockaddr_in seraddr; // 将结构体内存清零,防止出现随机垃圾数据 bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; // 指定地址族为IPv4 seraddr.sin_port = htons(50000); // 绑定端口50000,htons:主机字节序转网络大端序 seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址 // 将套接字文件描述符与IP、端口进行绑定 // 只有绑定后客户端才能通过这个IP+端口找到服务器 if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0) { perror("bind fail"); return -1; } // 开启监听,让套接字变成被动套接字,可以接收客户端连接 // 第二个参数5:允许挂起的未完成连接最大队列长度为5 int ret = listen(fd,5); if(ret < 0) { perror("listen fail"); return -1; } struct sockaddr_in cliaddr; // 保存连接上来的客户端地址信息 socklen_t addrlen = sizeof(cliaddr); // 客户端地址结构体长度 while(1) // 服务器死循环,循环接收新客户端连接 { // 阻塞等待客户端连接 // fd:监听套接字;cliaddr:输出参数,带回客户端IP端口;addrlen:地址长度 int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen); if(connfd < 0) { perror("accept fail"); return -1; } printf("accept success\n"); printf("--------------------\n"); printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); // 网络字节序IP转字符串打印 printf("port = %d\n",ntohs(cliaddr.sin_port)); // 网络端口转主机字节序打印 printf("--------------------\n"); // 创建子进程,实现多客户端并发 // 父进程:继续回去accept等待下一个客户端 // 子进程:专门负责和当前这个客户端收发数据 pid_t pid = fork(); if(pid == 0) // ============子进程分支============ { // 子进程循环和客户端通信 while(1) { char buf[1024] = {'\0'}; // 从客户端读取数据,阻塞等待客户端发送消息 read(connfd,buf,sizeof(buf)); // 判断客户端发来quit,关闭连接,子进程退出 if(strncmp(buf,"quit",4) == 0) { close(connfd); exit(0); } printf("ser:%s\n",buf); // 服务器打印收到的客户端消息 char sbuf[1024]; sprintf(sbuf,"server+%s\n",buf); // 拼接回复字符串 write(connfd,sbuf,strlen(sbuf)+1); // 把拼接好的数据发回客户端 } } // ============父进程分支============ // fork返回>0,父进程不会进入上面if,直接回到while循环,再次执行accept // 父进程不负责数据收发,只负责不断等待新客户端接入 } return 0;}
练习:多线程并发服务器,实现服务器回发客户端传送来的消息
-
socket () 创建监听套接字 fd 操作系统返回监听文件描述符,专门用来等待客户端连接。
-
填充服务器地址结构体 seraddr 配置服务器 IP:
192.168.31.94,端口:50000;端口使用htons转换成网络大端字节序。 -
bind () 绑定套接字与 IP 端口 客户端就可以通过
192.168.31.94:50000找到服务器程序。 -
listen () 开启监听 套接字由主动套接字 → 被动监听套接字,开始监听客户端 TCP 连接请求。
-
进入 while (1) 死循环,循环接收客户端
accept(fd,...)阻塞休眠,没有客户端连接时程序停在这里。- 客户端发起 TCP 连接、三次握手完成后,accept 返回全新通信套接字
connfd,这个 fd 专门用来和该客户端收发数据。 - 打印客户端 IP 和端口信息。
-
pthread_create 创建子线程处理客户端
- 主线程(main) :创建完子线程后,不会阻塞;立刻回到循环头部,再次执行
accept等待下一个客户端接入。 - 子线程 :执行
pthread_cli函数,接管connfd,专门负责和当前客户端循环收发消息。
-
pthread_detach(tid) 标记线程为分离线程。当子线程执行完毕退出时,系统自动回收线程资源,不需要主线程调用
pthread_join等待回收,避免产生僵尸线程。 -
子线程通信循环 pthread_cli
-
read(connfd,buf)阻塞读取客户端发送的数据; -
如果读到字符串
quit:关闭connfd套接字,跳出循环,pthread_exit(NULL)线程结束; -
没有收到 quit:服务器打印收到的消息,拼接回复字符串
server+xxx,write发送回客户端; -
继续循环等待客户端下一条消息。
#include<stdio.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>
#include<pthread.h>/**
-
@brief 子线程回调函数:专门和一个客户端进行数据通信
-
@param arg 传入通信套接字connfd的地址
-
@return 线程返回值
*/
void *pthread_cli(void * arg)
{
// 取出主线程传过来的通信文件描述符
int connfd = *(int *)arg;// 和客户端循环收发数据
while(1)
{
char buf[1024] = {'\0'};
// 阻塞读取客户端发送过来的数据
read(connfd,buf,sizeof(buf));// 如果客户端发送quit,关闭套接字,退出循环,线程结束 if(strncmp(buf,"quit",4) == 0) { close(connfd); break; } printf("ser:%s\n",buf); char sbuf[1024]; // 拼接回复字符串 sprintf(sbuf,"server+%s\n",buf); // 将响应数据写回客户端 write(connfd,sbuf,strlen(sbuf)+1);}
// 线程正常退出
pthread_exit(NULL);
}
int main(int argc, const char *argv[])
{
// 创建流式套接字,IPv4,TCP协议
// AF_INET:IPv4;SOCK_STREAM:TCP流式套接字;0自动匹配TCP协议
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
// 创建套接字失败打印错误信息
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);// 定义服务器地址结构体,存放服务器IP、端口信息 struct sockaddr_in seraddr; // 将结构体内存清零,清除随机脏数据 bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; // 指定地址族为IPv4 seraddr.sin_port = htons(50000); // 绑定端口50000,htons主机字节序转网络大端序 seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址 // 将监听套接字fd与IP、端口进行绑定 if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0) { perror("bind fail"); return -1; } // 开启监听,套接字变为被动套接字,等待客户端连接 // 第二个参数5:未完成连接队列最大长度 int ret = listen(fd,5); if(ret < 0) { perror("listen fail"); return -1; } struct sockaddr_in cliaddr; // 存储接入客户端的IP、端口 socklen_t addrlen = sizeof(cliaddr); // 客户端地址结构体大小 // 服务器主循环,不断等待新客户端接入 while(1) { // accept阻塞等待客户端TCP连接,三次握手成功后返回通信套接字connfd int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen); if(connfd < 0) { perror("accept fail"); return -1; } printf("accept success\n"); printf("--------------------\n"); printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); // 打印客户端IP printf("port = %d\n",ntohs(cliaddr.sin_port)); // 打印客户端端口 printf("--------------------\n"); pthread_t tid; // 创建子线程,让子线程处理当前这个客户端的通信业务 // 参数:线程id,线程属性,线程入口函数,传给线程函数的参数 pthread_create(&tid,NULL,pthread_cli,(void *)&connfd); // 设置线程分离态:线程结束后自动释放自身资源,不需要主线程pthread_join回收 pthread_detach(tid); } return 0;}
-
进程版 VS 线程版对比
| 项目 | fork 多进程服务器 | pthread 多线程服务器 |
|---|---|---|
| 资源开销 | 大,每个进程独立地址空间 | 小,线程共享主线程地址空间 |
| 文件描述符 | fork 会复制 fd 副本 | 所有线程共享文件描述符 |
| 数据隔离 | 进程间数据天然隔离 | 共享全局变量,需要加锁 |
| 回收资源 | SIGCHLD + wait 回收僵尸进程 | pthread_detach 回收线程 |
什么是 IO 多路复用
一个进程,通过一块内核监控,同时监听多个文件描述符 (fd),哪个 fd 就绪(可读 / 可写 / 异常),就去处理哪一个。
- 不需要创建进程、线程;单进程就可以并发处理多个客户端
- 三大模型:
select、poll、epoll - 三种并发方案对比
| 方案 | 并发方式 | 优点 | 缺点 |
|---|---|---|---|
| fork 多进程服务器 | 一客户端一子进程 | 编程简单,进程隔离稳定 | 资源开销巨大;进程切换消耗高;僵尸进程问题 |
| pthread 多线程服务器 | 一客户端一线程 | 开销比进程小 | 线程竞态、共享资源需要锁;多线程调度开销 |
| IO 多路复用 (select/epoll) | 单进程监控所有 fd | 资源开销最小;没有进程线程切换开销 | 代码逻辑复杂;单进程 CPU 只能跑一个业务,耗时任务会阻塞整个服务器 |
阻塞 IO‑Blocking‑IO
发起 IO 调用后,线程 / 进程立刻休眠,一直等到数据就绪、完成读写,函数才返回。
以 read(fd,buf,len) 读取一个没有数据的 socket 举例:
- 调用
read(),内核发现缓冲区没有数据 - 用户线程被挂起、让出 CPU,阻塞等待
- 内核收到网络数据,拷贝到用户缓冲区
- read 返回,程序继续往下执行
👉 特点:调用不返回,卡死在这里等。
非阻塞 IO‑Non‑Blocking‑IO
IO 调用立刻返回 ,不管数据有没有准备好。 如果数据没就绪,函数返回错误标记(Linux 里一般返回‑1,errno = EAGAIN / EWOULDBLOCK),不会卡住线程。
同样 read(fd,buf,len),socket 设为非阻塞:
- 调用 read (),内核缓冲区没有数据
- 马上返回,报错 EAGAIN,不阻塞线程
- 用户代码可以去干别的事,之后轮询 (polling) 再次调用 read 尝试读取
👉 特点:不会卡住,但是需要不停循环重试,也就是轮询。
fcntl 函数原型
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:
修改指定文件的属性信息。
参数:
fd 要调整的文件描述符
cmd 要调整的文件属性宏名称
... 可变长的属性值参数。
返回值:
成功 不一定,看cmd
失败 -1;
eg:
修改文件的非阻塞属性:
int flag ;
flag = fcntl(fd,F_GETFL,0); ///获取fd文件的默认属性到flag变量中。
flag = flag | O_NONBLOCK; ///将变量的值调整并添加非阻塞属性
fcntl(fd,F_SETFL,flag); ///将新属性flag设置到fd对应的文件生效。
以上代码执行后的阻塞IO将变成非阻塞方式。
学会设置文件IO的属性之后,我们继续学习,信号驱动IO原理。当对应的文件描述符可以执行 IO操作时。进程请求内核为自己发送一个信号。这就可以实现,进程执行任何其他的任务直到 I/O 就绪为止,此时内核会发送信号给进程。发送的信号为SIGIO信号,通过前面学习的signal()函数对信号处理进行注册sighandler即可实现,信号驱动IO
工作原理
-
进程给文件描述符设置 O_NONBLOCK + O_ASYNC(异步信号标志)
-
使用
fcntl(F_SETOWN)指定:数据就绪时,内核该通知哪个进程 / 线程 -
进程设置
SIGIO信号处理函数(signal/sigaction) -
进程就可以去干别的事情,不需要阻塞等待,也不用轮询
-
当内核缓冲区数据就绪时 → 内核发送 SIGIO 信号通知你的程序
-
程序收到信号,在信号回调函数里面执行 read/recv,读取数据
-
核心:内核主动发信号通知你数据来了,而不是你不停轮询问内核。
int flag;
flag = fcntl(connfd,F_GETFL,0);
flag = flag | O_NONBLOCK;
fcntl(connfd,F_SETFL,flag);O_NONBLOCK → 非阻塞开关
O_ASYNC → 异步信号驱动开关
练习:用信号驱动io实现服务器回发客户端发送来的信息
-
客户端发送数据到服务器
-
内核检测到套接字缓冲区有数据,发送
SIGIO信号给服务器进程 -
进程收到信号,打断当前主程序流程,跳转到
sighandler() -
在信号函数内部调用
read(cfd)读取客户端数据 -
判断如果收到
quit,关闭套接字、进程退出; -
否则拼接字符串
server+xxx,write 回写给客户端 -
信号处理函数执行完毕,回到主循环继续等待下一个客户端连接
int cfd = -1; // 全局变量:保存客户端文件描述符,信号处理函数需要访问
/**
-
@brief SIGIO 信号的回调处理函数
-
当客户端有数据到达,驱动发送SIGIO信号触发此函数,读取客户端数据并回复 -
@param signo 触发的信号编号
*/
void sighandler(int signo)
{
char buf[1024] = {'\0'};
// 从客户端套接字读取数据
int ret = read(cfd,buf,sizeof(buf));printf("ret = %d buf = %s\n",ret,buf);
// 如果客户端发送 quit,则关闭连接、退出进程
if(strncmp(buf,"quit",4) == 0)
{
close(cfd);
exit(0);
}printf("ser:%s\n",buf);
char sbuf[1024];
sprintf(sbuf,"server+%s\n",buf);
// 将拼接好的数据发回客户端
write(cfd,sbuf,strlen(sbuf)+1);return ;
}// 提前注册SIGIO异步IO信号的回调函数
// 当套接字有IO事件(收到数据),内核发送SIGIO信号,调用sighandler
signal(SIGIO,sighandler);// 循环等待客户端连接
while(1)
{
// 5.阻塞等待客户端连接,获取客户端套接字文件描述符
int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen);
if(connfd < 0)
{
perror("accept fail");
return -1;
}cfd = connfd; // 将客户端描述符赋值给全局变量,供信号函数使用 printf("accept success\n"); printf("--------------------\n"); printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); printf("port = %d\n",ntohs(cliaddr.sin_port)); printf("--------------------\n"); // 6.F_SETOWN:设置该文件描述符的信号接收者为本进程 // 当connfd产生IO事件时,内核把SIGIO信号发给当前进程PID fcntl(connfd,F_SETOWN,getpid()); // 7.获取套接字原来的文件状态标志 int flag; flag = fcntl(connfd,F_GETFL,0); // 8.追加 O_ASYNC(异步IO标志),开启信号驱动IO // 开启之后:套接字有数据可读 → 内核发送SIGIO信号 → 执行sighandler fcntl(connfd,F_SETFL,flag|O_ASYNC);}
-
信号IO能够实现非阻塞非轮询的异步操作IO,但同时也存在多路IO无法处理的问题,因此引入IO的多路复用函数select()
select 属于IO 多路复用 。 进程把一批文件描述符 (fd) 交给内核;然后阻塞在select(),由内核监视这些 fd;只要其中一个或多个 fd 就绪 (可读 / 可写 / 异常),select 就返回,用户程序再遍历所有 fd 处理 IO。
进程 → select(阻塞) → 内核监听多个fd
↓
某个fd数据就绪 → select唤醒返回
↓
用户循环扫描所有fd,找出就绪的
select函数原型:
int select( int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
功能:
实现IO多路复用
监控多路IO,看哪一路IO就绪
参数:
@nfds //最大的文件描述符+1(表示的是三个集合中最大的文件描述符加+1)
@readfds //存放 关心 读操作 的哪些文件描述符
@writefds //存放 关心 写操作 的哪些文件描述符
@exceptfds //存放 关心 出错操作 的哪些文件描述符
@timeout //设置一个超时时间
//NULL --- 表示select 阻塞调用
//时间为0 --- 表示select 非阻塞
延时时间的结构体struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
说明:
这个超时时间
递减,直到减成0
t中的值 就已经是0
返回值:
成功 返回 !!!就绪的文件描述符的数量!!! 因此要备份nfds
失败 返回-1 && errno会被设置
同时,为了适配select函数的执行,有以下的宏函数
为了配合select函数执行,有如下宏函数:
void FD_CLR(int fd, fd_set *set);
功能:将指定的set集合中编号为fd的描述符号删除。
int FD_ISSET(int fd, fd_set *set);
功能:判断值为fd的描述符是否在set集合中,
如果在则返回真,否则返回假。
void FD_SET(int fd, fd_set *set);
功能:将指定的fd描述符,添加到set集合中。
void FD_ZERO(fd_set *set);
功能:将指定的set集合中所有描述符删除。
set其实就是一个表,类型为fd_set的表,用来存储要监控的文件描述如有哪些,有一个,就用一次宏函数FD_SET写入到集合里,然后在调用select来监控。因为每次调用select都会返回响应了的文件描述符的集合,所以传入set都需要进行备份,不然循环监控,响应一次之后,后面都不监控了,只监控上一次select中响应了的文件描述符fd。
作业:用select多路复用IO实现服务器与客户端点对点实时聊天,双向通信。
-
初始化 select 文件描述符集合 监听两个可读事件:
- fd=0:键盘标准输入,服务器控制台输入消息发给客户端。
- connfd:客户端套接字,接收客户端发来的消息。
-
fcntl 设置 0、connfd 为非阻塞 IO read、fgets、write 不会卡住程序。
-
进入 while (1) 永久循环
- 每次循环先把备份好的
backfds重新赋值给rdfds。select 会修改 rdfds,就绪以外的 fd 会被清除,所以每次都要重置。 - 调用
select(),超时时间 1000 微秒。ret<0:select 出错,程序退出ret==0:超时,没有键盘输入、也没有客户端消息;回到循环开头重新 selectret>0:有文件描述符就绪,开始遍历检查就绪 fd
- 每次循环先把备份好的
-
遍历判断就绪的文件描述符
- 如果就绪 fd == 0:读取键盘输入,发送字符串给客户端;输入
quit关闭套接字,程序退出。 - 如果就绪 fd == connfd:读取客户端发来的数据,打印在服务器终端;客户端发送
quit,服务器关闭套接字退出。
- 如果就绪 fd == 0:读取键盘输入,发送字符串给客户端;输入
-
一轮收发完成后,回到 while 循环顶部,继续等待下一次可读事件。
#include<stdio.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<signal.h>
#include<fcntl.h>int main(int argc, const char *argv[])
{
// 1.创建TCP流式套接字,IPv4协议族,TCP(SOCK_STREAM),协议号自动填0
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd < 0)
{
// 创建套接字失败打印错误信息
perror("socket fail");
return -1;
}
printf("fd = %d\n",fd);// 定义服务器地址结构体,存放服务器IP+端口 struct sockaddr_in seraddr; // 将结构体内存全部清零,防止垃圾数据 bzero(&seraddr,sizeof(seraddr)); seraddr.sin_family = AF_INET; // 指定地址族为IPv4 seraddr.sin_port = htons(50000); // 绑定端口50000,主机字节序转网络大端序 seraddr.sin_addr.s_addr = inet_addr("192.168.31.94"); // 绑定本机指定IP地址 // 将套接字fd与IP、端口进行绑定 if(bind(fd,(const struct sockaddr *)&seraddr,sizeof(seraddr)) < 0) { perror("bind fail"); return -1; } // 开启监听,设置内核等待队列最大挂起连接数为5 int ret = listen(fd,5); if(ret < 0) { perror("listen fail"); return -1; } struct sockaddr_in cliaddr; // 用来保存连接上来的客户端地址信息 socklen_t addrlen = sizeof(cliaddr); // 阻塞等待客户端连接,成功返回新通信文件描述符connfd int connfd = accept(fd,(struct sockaddr *)&cliaddr,&addrlen); if(connfd < 0) { perror("accept fail"); return -1; } printf("accept success\n"); printf("--------------------\n"); printf("cliaddr id = %s\n",inet_ntoa(cliaddr.sin_addr)); //打印客户端IP printf("port = %d\n",ntohs(cliaddr.sin_port)); //打印客户端端口 printf("--------------------\n"); fd_set rdfds; // 可读文件描述符集合,select要用 FD_ZERO(&rdfds); // 清空文件描述符集合 FD_SET(0,&rdfds); // 将标准输入(键盘,fd=0)加入监听集合 FD_SET(connfd,&rdfds);// 将客户端通信套接字connfd加入监听集合 fd_set backfds = rdfds; // 备份原始的文件描述符集合(select每次会清空就绪位) char buf[1024]; // 收发数据缓冲区 int nfds = connfd + 1; // select第一个参数:最大文件描述符+1 //==== 设置标准输入为非阻塞 ==== int flag = fcntl(0,F_GETFL); // 获取0号文件描述符原有属性 fcntl(0,F_SETFL,flag | O_NONBLOCK); // 添加非阻塞标志 //==== 设置客户端套接字connfd为非阻塞 ==== flag = fcntl(connfd,F_GETFL); fcntl(connfd,F_SETFL,flag | O_NONBLOCK); // 循环监听键盘输入 和 客户端发来的数据 while(1) { rdfds = backfds; // 每次循环重置监听集合,select会修改rdfds struct timeval t = {0,1000}; // select超时时间:0秒 + 1000微秒 int ret = select(nfds,&rdfds,NULL,NULL,&t); //select只监听可读事件 if(ret < 0) { perror("select fail"); return -1; } // ret>0:有文件描述符就绪(有数据可读) if(ret > 0) { int i = 0; for(i=0;i<nfds;i++) //遍历所有被监听的fd { if(FD_ISSET(i,&rdfds)) //判断i这个文件描述符是否就绪 { if(i == 0) //就绪的是键盘标准输入 { fgets(buf,sizeof(buf),stdin); //读取键盘输入 write(connfd,buf,strlen(buf)-1); //发送给客户端,去掉回车符 if(strncmp(buf,"quit",4) == 0) //输入quit就退出程序 { close(fd); close(connfd); exit(0); } }else if(i == connfd) //就绪的是客户端套接字,客户端发来了消息 { int ret = read(connfd,buf,sizeof(buf)); //读取客户端数据 if(strncmp(buf,"quit",4) == 0) //客户端发quit,服务器退出 { close(fd); close(connfd); exit(0); } printf("cli to ser:%s\n",buf); //打印客户端消息 } } } } // ret==0:select超时,没有任何fd就绪,回到循环继续select } return 0;}