目录
TCP相关概念
TCP是一个面向连接的、可靠的、流式服务。
- 面向链接指我们需要通过三次握手来进行链接的建立,通过四次挥手来进行链接断开。握手不可以两次,挥手可以三次(具体原因上节已经讲过)
- 可靠性怎末保证?我们通过应答确认超时重传,或者去重机制、乱序重排、滑动窗口进行流量控制。
- 流式服务本身的特点会出现一个粘包现象,因为我们每次发送一个数据,并不是独立的包,属于整数流的一部分,因此在TCP在进行发送数据的时候,经过两次或多次发送数据有可能会被对方一次收到,即发送次数和链接次数不对应或者不固定。
TCP并发处理多客户端的方法
多线程模型
为每个客户端连接创建一个独立的线程处理I/O操作。线程池可优化资源消耗,避免频繁创建销毁线程。
示例代码(C):
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include<pthread.h>
//多线程
int socket_init();
void* fun(void *arg){
int *p=(int*)arg;
int c=*p;
free(p);
while(1){//循环结束客户端的连接
char buff[128]={0};
int n=recv(c,buff,127,0);
if(n<=0){//客户端关闭,退出循环
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);
}
printf("client close\n");
}
int main(){
int sockfd = socket_init();
if(sockfd==-1){
exit(1);
}
while( 1 ){
struct sockaddr_in caddr; // 记录客户端地址
socklen_t len = sizeof(caddr); // 计算大小
// 从已完成握手的监听队列处理一个链接,队列空,阻塞,
int c = accept(sockfd, (struct sockaddr *)&caddr, &len); // 接受客户端连接
if (c < 0){
continue;
}
printf("c=%d,ip:%s,port=%d\n", c, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// ip转化为点分十进制的字符串,端口将网络字节序列转化为主机字节序列
pthread_t id;
int *p = (int *)malloc(sizeof(int));
if (p == NULL){
close(c);
continue;
}
*p=c;
pthread_create(&id, NULL, fun, (void *)p);
}
}
int socket_init(){//创建套接字
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 ){
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;//填充地址图
saddr.sin_port = htons(6000);//填充端口,大端,主机字节序列转为网络字节序列,
//1024以内知名端口,root;1024-4096保留端口一般不用;4096以上为临时端口,随便用
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址,转为无符号整型
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1){
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if( res == -1){
return -1;
}
return sockfd;
}
多进程模型
每个客户端连接由独立进程处理,隔离性更好但资源消耗更高。适用于CPU密集型场景。
注意:如果子进程结束而父进程没有结束,容易出现僵死进程,因此我们应采用wait去防止出现这种状态。
示例代码(C语言):
c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
//多进程
int socket_init();
void sig_fun(int sig){//信号处理
int val = 0;
wait(&val);
}
int main(){
int sockfd = socket_init();
if( sockfd == -1 ){
exit(1);
}
signal(SIGCHLD,sig_fun);
while( 1 ){
struct sockaddr_in caddr;//客户端的ip,port
socklen_t len = sizeof(caddr);
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//阻塞,有客户端连接解除阻塞
if( c < 0 ){
continue;
}
printf("accept c=%d,client port=%d\n",c,ntohs(caddr.sin_port));
pid_t pid = fork();//子进程
if( pid == -1){//无法创建新的进程,就关闭服务器
close(c);
continue;
}
if( pid == 0 ){//如果创建子进程成功,
while( 1 ){
char buff[128] = {0};
int n = recv(c,buff,127,0);
if( n <= 0 ){
break;
}
printf("buff=%s\n",buff);
send(c,"ok",2,0);
}
close(c);
printf("client close\n");
exit(0);
}
close(c); //父进程关闭c
}
}
int socket_init(){
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if( sockfd == -1 ){
return -1;
}
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1){
printf("bind err\n");
return -1;
}
res = listen(sockfd,5);
if( res == -1 ){
return -1;
}
return sockfd;
}

IO多路复用
使用select/poll/epoll监控多个文件描述符,单线程即可处理高并发连接。
epoll示例(Linux):
c
struct epoll_event ev, events[MAX_EVENTS];
int epoll_fd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
while(1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for(int i = 0; i < nfds; i++) {
if(events[i].data.fd == server_fd) {
// 处理新连接
} else {
// 处理客户端数据
}
}
}
异步IO模型
通过回调机制实现非阻塞处理,如libevent、boost.asio等库。
Python asyncio示例:
python
import asyncio
async def handle_client(reader, writer):
while True:
data = await reader.read(100)
if not data: break
writer.write(data)
await writer.drain()
writer.close()
async def main():
server = await asyncio.start_server(handle_client, '0.0.0.0', 8888)
async with server: await server.serve_forever()
asyncio.run(main())
边缘触发与水平触发
epoll支持两种模式:
- 边缘触发(ET):仅在状态变化时通知,需一次性处理完所有数据
- 水平触发(LT):只要满足条件就会持续通知,编程更简单
性能优化建议
- 设置SO_REUSEADDR选项避免TIME_WAIT状态影响
- 使用非阻塞socket配合IO多路复用
- 针对短连接场景调整TCP快速回收参数
- 负载过高时考虑添加应用层协议头标识包边界