目录
一、核心思路
服务器 accept 到一个客户端连接后,不自己处理数据,而是创建子进程 / 子线程去处理,主线程 / 主进程继续监听,实现同时处理多个客户端。
二、多进程并发(fork)
流程
- 主进程:socket → bind → listen → 循环 accept
- 来一个客户端就 fork ()
- 子进程:关闭 listenfd,和客户端读写
- 主进程:关闭 connfd,继续 accept
- 用 waitpid 回收子进程,避免僵尸进程
关键代码
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <sys/wait.h>
#include <signal.h>
typedef struct sockaddr *(SA);
void handle(int num)
{
wait(NULL);
}
int main(int argc, char **argv)
{
signal(SIGCHLD, handle);
//监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if(listfd == -1)
{
perror("listfd");
return 1;
}
//创建服务器,客户端地址结构体
struct sockaddr_in ser,cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
//给套接字绑定ip+port
int ret = bind(listfd,(SA)&ser,sizeof(ser));
if(ret == -1)
{
perror("bind");
return 1;
}
//进入监听状态
listen(listfd, 3);
socklen_t len = sizeof(cli);
//多个客户端三次握手
while(1)
{
//建立连接通信套接字
int conn = accept(listfd, (SA)&cli, &len);
if(conn == -1)
{
perror("accept");
close(conn);
continue;
}
//创建进程
pid_t pid = fork();
if(pid > 0)
{
//主进程只负责建立连接-关闭通信套接字
close(conn);
}
else if(pid == 0)
{
//关闭监听套接字
close(listfd);
while(1)//与客户端的多次收发
{
char buf[512] = {0};
//接收数据
int rd_ret = recv(conn, buf, sizeof(buf), 0);
if(rd_ret <= 0)
{
printf("cli offline\n");
exit(0);
}
//处理数据
printf("cli:%s\n",buf);
time_t tm;
time(&tm);
sprintf(buf,"%s %s",buf,ctime(&tm));
//发送数据
send(conn,buf,strlen(buf),0);
}
}
else
{
perror("fork");
continue;
}
}
close(listfd);
return 0;
}
优点
- 进程独立,一个挂了不影响其他
- 稳定、安全
缺点
- 开销大,占内存多
- 进程间通信麻烦
三、多线程并发(pthread)
流程
- 主线程:socket → bind → listen → 循环 accept
- 来一个客户端就 pthread_create
- 线程函数处理读写
- 设置线程分离,自动释放资源
关键代码
cs
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
sem_t sem_arg;
typedef struct sockaddr*(SA);
void* th(void* arg)
{
int conn = *(int*)arg;
//设置信号量,确保conn不被下一个conn号覆盖
sem_post(&sem_arg);
//分离属性,当线程结束后,栈区自动由系统回收
pthread_detach(pthread_self());
//服务器收发客户端数据过程
while(1)
{
char buf[512] = {0};
// 5 接收数据
// 返回值 >0 实际收到的字节数 ==0 对方断开 -1 错误
int rd_ret = recv(conn, buf, sizeof(buf), 0);
if (rd_ret <= 0)
{
printf("cli offline\n");
close(conn);
break;
}
// 5.5 处理数据
printf("cli:%s\n", buf);
time_t tm;
time(&tm);
sprintf(buf, "%s %s", buf, ctime(&tm));
// 6 发送数据
send(conn, buf, strlen(buf), 0);
}
return NULL;
}
int main(int argc, char **argv)
{
//监听套接字
int listfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == listfd)
{
perror("socket");
return 1;
}
//创建服务器,客户端地址结构体
struct sockaddr_in ser, cli;
//清空 结构体
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = INADDR_ANY;
//给套接字绑定ip+port
int ret = bind(listfd, (SA)&ser, sizeof(ser));
if (-1 == ret)
{
perror("bind");
return 1;
}
//进入监听状态
listen(listfd, 3);
socklen_t len = sizeof(cli);
//设置信号量
sem_init(&sem_arg,0,0);
while(1)
{
//通信套接字
int conn = accept(listfd, (SA)&cli, &len);
if(conn == -1)
{
perror("accept");
close(conn);
continue;
}
//创建线程
pthread_t tid;
pthread_create(&tid, NULL, th, &conn);
//确保conn参数一定在th线程中被保存下来
sem_wait(&sem_arg);
}
sem_destroy(&sem_arg);
close(listfd);
return 0;
}
优点
- 资源开销小
- 共享全局变量,通信方便
缺点
- 线程共享资源,要加锁
- 一个线程崩溃整个进程挂掉
四、简单对比
- fork:稳定、耗资源、独立
- pthread:轻量、高效、要处理同步