1.使用线程池
优点是简单,易于管理,多线程对运算密集型访问更友好,缺点为线程的上下文切换开销大,需要考虑线程安全,数据访问的竞态条件
实现方法如下:
//创建本地服务
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2048);
if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))) {
perror("bind");
return -1;
}
listen(sockfd, 10);
//对每条连接创建线程
while (1) {
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
printf("阻塞在accept");
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
pthread_t thid;
pthread_create(&thid, NULL, client_thread, &clientfd);
}
//针对每条连接做解析
void *client_thread(void *arg) {
int clientfd = *(int *)arg;
while (1) {
char buffer[128] = {0};
int count = recv(clientfd, buffer, 128, 0);
if (count == 0) {
break;
}
//
send(clientfd, buffer, count, 0);
printf("clientfd: %d, count: %d, buffer: %s\n", clientfd, count, buffer);
}
close(clientfd);
}
2.使用select、poll
优点:比线程开销小,适合I/O密集型任务,开关延迟低,不适合运算密集型任务
缺点:管理的端口数有上限(默认1024个),每次建立连接需要复制所有fp状态到内核,速度慢
poll 实现方法:
struct pollfd fds[1024] = {0};//建立poll池
fds[sockfd].fd = sockfd;//将服务fd加入到poll池中
fds[sockfd].events = POLLIN;//设置输入事件为POLLIN,表示关注读取
int maxfd = sockfd;//初始化最大访问的文件描述符
while (1) {
int nready = poll(fds, maxfd+1, -1);//等待一个新的连接,-1表示无超时
if (fds[sockfd].revents & POLLIN) { //如果fd的返回事件是读取
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//建立客户端连接
printf("sockfd: %d\n", clientfd);
fds[clientfd].fd = clientfd;//将客户端fd加入poll池
fds[clientfd].events = POLLIN;//关注读取
maxfd = clientfd;
}
int i = 0;
for (i = sockfd+1;i <= maxfd;i ++ ) {//遍历新的链接
if (fds[i].revents & POLLIN) {//如果fd的返回事件是读取
char buffer[128] = {0};
int count = recv(i, buffer, 128, 0);//读取一次数据
if (count == 0) {
printf("disconnect\n");
fds[i].fd = -1;//从poll池中清除本次连接
fds[i].events = 0;//事件输入为空
close(i);//关闭链接
continue;
}
send(i, buffer, count, 0);//返回数据
printf("clientfd: %d, count: %d, buffer: %s\n", i, count, buffer);
}
}
}
3.使用epoll
优点:I/O速度很快,缺点:可读性不如旧一些的方法
实现方法:
int epfd = epoll_create(1); // 初始化信息缓存空间
//pthread_create();
struct epoll_event ev; //初始化服务端事件
ev.events = EPOLLIN; //设置为输入
ev.data.fd = sockfd; //设置fd
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); //将服务端、缓存空间、事件绑定
struct epoll_event events[1024] = {0}; //建立事件传输的缓存
while (1) {
int nready = epoll_wait(epfd, events, 1024, -1); //等待一个新的连接
int i = 0;
for (i = 0;i < nready;i ++) {//遍历所有连接
int connfd = events[i].data.fd;
if (sockfd == connfd) {//如果连接的是本程序的服务
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);//建立服务端的连接
ev.events = EPOLLIN | EPOLLET; //事件关注读取、边沿触发模式(发生状态变化时触发,默认为水平触发,即读完为止)
ev.data.fd = clientfd;//事件绑定用户fd
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);//绑定用户、事件、缓存空间
printf("clientfd: %d\n", clientfd);
} else if (events[i].events & EPOLLIN) {
char buffer[10] = {0};
int count = recv(connfd, buffer, 10, 0);//将数据读出来
if (count == 0) {
printf("disconnect\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL); //解绑用户、缓存空间,事件置空
close(i);//关闭端口
continue;
}
send(connfd, buffer, count, 0);//向用户返回数据
printf("clientfd: %d, count: %d, buffer: %s\n", connfd, count, buffer);
}
}
}
这里强调一下,关于触发模式,对于比较大的数据发送更适合用边沿触发,以保证传输效率,对大部分情况适合用水平触发以保证主句完整及易于解读。
总结:
如果处理每个访问需要运算密集数据则适合使用线程池,如果处理I/O密集型任务则适合使用epoll,select和poll的代码更为简洁,易于管理,对轻小型任务可以考虑使用,对于大规模并发任务不如epoll或线程池表现强。