IO多路复用机制select实现TCP服务器
一、前言
手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。
为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。
本节,在上一章节的基础上,将并发的实现改为IO多路复用机制,使用select管理每个新接入的客户端连接,实现发送和接收。
二、新增使用API函数
2.1、select()函数
函数原型:
c
#include <sys/types.h>
#include <unistd.h>
int select(int maxfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
select函数共有5个参数,其中参数:
- maxfds:监视对象文件描述符数量。
- readset:将所有关注"是否存在待读取数据"的文件描述符注册到fd_set变量,并传递其地址值。
- writeset: 将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set变量,并传递其地址值。
- exceptset:将所有关注"是否发生异常"的文件描述符注册到fd_set变量,并传递其地址值。
- timeout:调用select后,为防止陷入无限阻塞状态,传递超时信息。
返回值:
- 错误返回-1。
- 超时返回0。
当关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
2.2、FD_*系列函数
函数原型:
c
#include <sys/types.h>
#include <unistd.h>
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
(1)FD_CLR函数用于将fd从set集合中清除,即不监控该fd的事件。
(2)FD_SET函数用于将fd添加到set集合中,监控其事件。
(3)FD_ZERO函数用于将set集合重置。
(4)FD_ISSET函数用于判断set集合中的fd是否有事件(读、写、错误)。
三、实现步骤
什么是IO多路复用?通俗的讲就是一个线程,通过记录IO流的状态来管理多个IO。解决创建多个进程处理IO流导致CPU占用率高的问题。
select是io多路复用的一种方式,其他的还有poll、epoll等。
(1)创建socket。
c
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
(2)绑定地址。
c
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
(3)设置监听。
c
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
(4)初始化可读文件描述符集合,将监听套接字加入集合。
c
fd_set writefds,readfds,wset,rset;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd,&readfds);
(5)从可读文件描述符集合中选择一个就绪的套接字。
c
wset=writefds;
rset=readfds;
// 从可读文件描述符集合中选择就绪的套接字
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
if(nready==-1)
{
printf("select errno = %d, %s\n",errno,strerror(errno));
continue;
}
(6)如果监听套接字有新连接请求,处理新连接。
c
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("accept errno = %d, %s\n",errno,strerror(errno));
}
else{
printf("accept successdul, clientfd = %d\n",clientfd);
// 将新套接字加入可读文件描述符集合
FD_SET(clientfd,&readfds);
if(clientfd>maxfd)
maxfd=clientfd;
}
(7)处理客户端发来的数据和发送数据到客户端。
c
int i=0;
for(i=listenfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&rset))
{
printf("recv fd=%d\n",i);
ret=recv(i,buf,BUFFER_LENGTH,0);
if(ret==0) {
// 客户端断开连接
printf("connection dropped\n");
// 从可读文件描述符集合中移除该套接字
FD_CLR(i,&readfds);
close(i);
}
else if(ret>0)
{
printf("fd=%d recv --> %s\n",i,buf);
FD_CLR(i,&readfds);
FD_SET(i,&writefds);
}
}
else if(FD_ISSET(i,&wset))
{
printf("send to fd=%d\n",i);
ret=send(i,buf,ret,0);
if(ret==-1)
{
printf("send() errno = %d, %s\n",errno,strerror(errno));
}
FD_CLR(i,&writefds);
FD_SET(i,&readfds);
}
}
四、完整代码
c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#define LISTEN_PORT 9999
#define BLOCK_SIZE 10
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_BIND_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4,
SOCKET_SELECT_FAILED=-5
};
int main(int argc,char **argv)
{
// 1.
int listenfd=socket(AF_INET,SOCK_STREAM,0);
if(listenfd==-1){
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
// 2.
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(LISTEN_PORT);
if(-1==bind(listenfd,(struct sockaddr*)&server,sizeof(server))){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_BIND_FAILED;
}
// 3.
if(-1==listen(listenfd,BLOCK_SIZE)){
printf("errno = %d, %s\n",errno,strerror(errno));
close(listenfd);
return SOCKET_LISTEN_FAILED;
}
printf("listen port: %d\n",LISTEN_PORT);
fd_set writefds,readfds,wset,rset;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_SET(listenfd,&readfds);
char buf[BUFFER_LENGTH]={0};
int ret=0;
int maxfd=listenfd;
while(1)
{
wset=writefds;
rset=readfds;
// 从可读文件描述符集合中选择就绪的套接字
int nready=select(maxfd+1,&rset,&wset,NULL,NULL);
if(nready==-1)
{
printf("select errno = %d, %s\n",errno,strerror(errno));
continue;
}
// 如果监听套接字有新连接请求,处理新连接
if(FD_ISSET(listenfd,&rset))
{
// 4.
printf("accept , listenfd = %d\n",listenfd);
struct sockaddr_in client;
memset(&client,0,sizeof(client));
socklen_t len=sizeof(client);
int clientfd=accept(listenfd,(struct sockaddr*)&client,&len);
if(clientfd==-1){
printf("accept errno = %d, %s\n",errno,strerror(errno));
}
else{
printf("accept successdul, clientfd = %d\n",clientfd);
// 将新套接字加入可读文件描述符集合
FD_SET(clientfd,&readfds);
if(clientfd>maxfd)
maxfd=clientfd;
}
}
printf("listenfd=%d.maxfd=%d\n",listenfd,maxfd);
int i=0;
for(i=listenfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&rset))
{
printf("recv fd=%d\n",i);
ret=recv(i,buf,BUFFER_LENGTH,0);
if(ret==0) {
// 客户端断开连接
printf("connection dropped\n");
// 从可读文件描述符集合中移除该套接字
FD_CLR(i,&readfds);
close(i);
}
else if(ret>0)
{
printf("fd=%d recv --> %s\n",i,buf);
FD_CLR(i,&readfds);
FD_SET(i,&writefds);
}
}
else if(FD_ISSET(i,&wset))
{
printf("send to fd=%d\n",i);
ret=send(i,buf,ret,0);
if(ret==-1)
{
printf("send() errno = %d, %s\n",errno,strerror(errno));
}
FD_CLR(i,&writefds);
FD_SET(i,&readfds);
}
}
}
close(listenfd);
return 0;
}
编译命令:
bash
gcc -o server server.c
五、TCP客户端
5.1、自己实现一个TCP客户端
自己实现一个TCP客户端连接TCP服务器的代码:
c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFFER_LENGTH 1024
enum ERROR_CODE{
SOCKET_CREATE_FAILED=-1,
SOCKET_CONN_FAILED=-2,
SOCKET_LISTEN_FAILED=-3,
SOCKET_ACCEPT_FAILED=-4
};
int main(int argc,char** argv)
{
if(argc<3)
{
printf("Please enter the server IP and port.");
return 0;
}
printf("connect to %s, port=%s\n",argv[1],argv[2]);
int connfd=socket(AF_INET,SOCK_STREAM,0);
if(connfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
return SOCKET_CREATE_FAILED;
}
struct sockaddr_in serv;
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=inet_addr(argv[1]);
serv.sin_port=htons(atoi(argv[2]));
socklen_t len=sizeof(serv);
int rwfd=connect(connfd,(struct sockaddr*)&serv,len);
if(rwfd==-1)
{
printf("errno = %d, %s\n",errno,strerror(errno));
close(rwfd);
return SOCKET_CONN_FAILED;
}
int ret=1;
while(ret>0)
{
char buf[BUFFER_LENGTH]={0};
printf("Please enter the string to send:\n");
scanf("%s",buf);
send(connfd,buf,strlen(buf),0);
memset(buf,0,BUFFER_LENGTH);
printf("recv:\n");
ret=recv(connfd,buf,BUFFER_LENGTH,0);
printf("%s\n",buf);
}
close(rwfd);
return 0;
}
编译:
bash
gcc -o client client.c
5.2、Windows下可以使用NetAssist的网络助手工具
下载地址:http://old.tpyboard.com/downloads/NetAssist.exe
小结
至此,我们实现了一个使用IO多路复用机制实现的服务器,这时的TCP服务器可以使用一个线程就能处理多个客户端连接。通过记录IO流的状态来管理多个IO,解决创建多个进程处理IO流导致CPU占用率高的问题。
我们总结一下select的使用流程:
1、定义io管理状态变量:fd_set rfds,wfds;
2、初始化变量:FD_ZERO();
3、设置io流状态,最初只有监听的fd,将其设置:FD_SET(listenfd,rfds);
4、在循环中select。
5、FD_ISSET()判断端口是否有连接。
6、FD_ISSET()判断可读、可写状态。
select是io多路复用的一种方式,其他的还有poll、epoll等。下一章节我们将使用更高效的IO多路复用器epoll来实现TCP服务器。