参考于Linux高性能服务器编程,并对代码进行了深度剖析。
服务端
使用IO多路复用技术实现多个用户连接,主要功能是负责接受客户的数据,并将客户的数据发送给每一个登陆到该服务器上的客户端。
服务端代码
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#define users_LIMIT 5 //最大用户数量
#define BUFFER_SIZE 64 //读缓冲区的大小
#define FD_LIMIT 65535 //文件描述符数量限制
//客户数据:地址,待写入客户端数据的位置,从客户端读入的数据
struct client_data
{
sockaddr_in address;
char* write_buf;
char buf[BUFFER_SIZE];
};
int setnonblocking(int fd)
{
int old_flag = fcntl(fd,F_GETFL);
int new_flag = old_flag | O_NONBLOCK;
fcntl(fd,F_SETFL,new_flag);
return old_flag;
}
int main(int argc,char** argv)
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&address.sin_addr.s_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET,SOCK_STREAM,0);
assert(listenfd >= 0);
ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
assert(ret >= 0);
ret = listen(listenfd,5);
assert(ret >= 0);
//创建users数组,分配FD_LIMIT个对象,可以预期每个可能的socket连接都可以获得一个这样的对象,且socket的值直接作为索引
client_data* users = new client_data[FD_LIMIT];
//限制用户数量
pollfd fds[users_LIMIT+1];
int user_counter = 0;
for(int i = 1;i <= users_LIMIT;i++)
{
fds[i].fd = -1;
fds[i].events = 0;
}
fds[0].fd = listenfd;
fds[0].events = POLLIN|POLLERR;
fds[0].revents = 0;
while(1)
{
ret = poll(fds,user_counter+1,-1);
if(ret < 0)
{
printf("poll failure\n");
break;
}
for(int i = 0;i < user_counter+1;i++)
{
if((fds[i].fd == listenfd) && (fds[i].revents & POLLIN))
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
if(connfd < 0)
{
printf("errno is: %d\n",errno);
continue;
}
//请求过多,则关闭新到的连接
if(user_counter >= users_LIMIT)
{
const char* info = "too many users\n";
printf("%s",info);
send(connfd,info,strlen(info),0);
close(connfd);
continue;
}
//对于新的连接,同时修改fds和userss数组。
user_counter ++;
users[connfd].address = client_address;
setnonblocking(connfd);
fds[user_counter].fd = connfd;
fds[user_counter].events = POLLIN|POLLRDHUP|POLLERR;
fds[user_counter].revents = 0;
printf("comes a new user,now have %d users\n",user_counter);
}
else if(fds[i].revents & POLLERR)
{
printf("get an err from %d\n",fds[i].fd);
char errors[100];
memset(errors,'\0',sizeof(errors));
socklen_t length = sizeof(errors);
if(getsockopt(fds[i].fd,SOL_SOCKET,SO_ERROR,&errors,&length) < 0)
{
printf("get socket option failed\n");
}
continue;
}
else if(fds[i].revents & POLLRDHUP)
{
//如果客户端关闭连接,则服务器也关闭对应的连接,并将用户数减1;
users[fds[i].fd] = users[fds[user_counter].fd];
close(fds[i].fd);
fds[i] = fds[user_counter];
i--;
user_counter--;
printf("a client lefter\n");
}
else if(fds[i].revents & POLLIN)
{
int connfd = fds[i].fd;
memset(users[connfd].buf,'\0',BUFFER_SIZE);
ret = recv(connfd,users[connfd].buf,BUFFER_SIZE-1,0);
if(ret < 0)
{
//读操作出错,关闭连接
if(errno != EAGAIN || errno != EWOULDBLOCK)
{
close(connfd);
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
}
}
else if(ret == 0)
{
//此处不需要处理,因为注册了POLLRDHUB事件,客户端关闭了连接会触发该事件
}
else
{
//如果接收到客户端的数据,通知其他socket连接接收数据
for(int j = 1;j <= user_counter;j++)
{
if(fds[j].fd == connfd)
{
continue;
}
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
else if(fds[i].revents & POLLOUT)
{
int connfd = fds[i].fd;
if(!users[connfd].write_buf)
{
continue;
}
ret = send(connfd,users[connfd].write_buf,strlen(users[connfd].write_buf),0);
users[connfd].write_buf = NULL;
//写完数据需要重新注册fds[i]上的可读事件
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete [] users;
close(listenfd);
return 0;
}
客户端
- 从标准输入终端读入用户数据,并将用户数据发送到服务器
- 往标准输出终端打印服务器发给它的数据
客户端代码
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#define BUFFER_SIZE 64
int main(int argc,char** argv)
{
if(argc <= 2)
{
printf("usage:%s ip_address port_number\n",basename(argv[1]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in ser_address;
bzero(&ser_address,sizeof(ser_address));
ser_address.sin_family = AF_INET;
inet_pton(AF_INET,ip,&ser_address.sin_addr.s_addr);
ser_address.sin_port = htons(port);
//创建TCPsocket将其绑定在端口port上
int sockfd = socket(PF_INET,SOCK_STREAM,0);
assert(sockfd >= 0);
if(connect(sockfd,(struct sockaddr*)&ser_address,sizeof(ser_address)) < 0)
{
printf("connection failed\n");
close(sockfd);
return 1;
}
pollfd fds[2];
//注册文件描述符0(标准输入)和sockfd上的可读事件
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[0].revents = 0;
fds[1].fd = sockfd;
fds[1].events = POLLIN|POLLRDHUP;
fds[1].revents = 0;
char read_buf[BUFFER_SIZE];
//创建管道
int pipefd[2];
ret = pipe(pipefd);
assert(ret != -1);
while(1)
{
ret = poll(fds,2,-1);
if(ret < 0)
{
printf("poll failed\n");
break;
}
else if(fds[1].revents & POLLRDHUP)
{
printf("server close the connection\n");
break;
}
else if(fds[1].revents & POLLIN)
{
memset(read_buf,'\0',BUFFER_SIZE);
recv(fds[1].fd,read_buf,BUFFER_SIZE-1,0);
printf("%s\n",read_buf);
}
if(fds[0].revents & POLLIN)
{
//使用零拷贝技术将数据直接写到sockfd上
ret = splice(0,NULL,pipefd[1],NULL,37628,SPLICE_F_MORE|SPLICE_F_MOVE);
ret = splice(pipefd[0],NULL,sockfd,NULL,32768,SPLICE_F_MORE|SPLICE_F_MOVE);
}
}
close(sockfd);
return 0;
}
技术要点
- 零拷贝技术
- splice函数
- poll机制
- 对于客户端的断开连接并不是通过ret == 0来判断,对于ret == 0不做处理,而是注册POLLRDHUP事件,每当一个连接断开时,会触发POLLRDHUP事件,执行用户删除的逻辑
- 已断开连接用户的删除逻辑(重点体会)
close(fds[i].fd);
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
- 有两处要执行用户的删除逻辑分别是
- POLLRDHUP事件发生时
- POLLIN事件发生在读取数据的时候出错(ret < 0);
- 如何做到将消息通知除自己以外得所有在线socket
for(int j = 1;j <= user_counter;j++)
{
if(fds[j].fd == connfd)
{
continue;
}
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = users[connfd].buf;
}
- 为什么要关闭POLLIN事件?
- 从代码逻辑我们可以知道,POLLIN事件是先于POLLOUT事件被判断是否触发,为了提高消息的实时性,每当接受到客户端的数据时,服务端优先把数据发送给其他在线客户端。
- 为什么不在一开始就注册POLLOUT事件,而是收到客户端数据后开启该事件,发送完数据后又关闭该事件?
- 这是个大坑,要搞懂这个POLLOUT什么时候被触发,就需要我们来看看什么时候socket可写:
- socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0;
- socket 的写操作被关闭(调用了 close 或者 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号);
- socket 使⽤非阻塞 connect 连接成功或失败之后;
- 从上socket的可写条件我们可以看到POLLOUT是否被触发取决于发送缓冲区的可用字节数,而不是我们是否想写数据,也就是说即使我们不想写数据,只要发送缓冲区可用空间大小满足条件就会触发该事件。
- 综合上面所表达的内容我们也就知道了服务端是如何把消息发送给其他客户端的。
- 这是个大坑,要搞懂这个POLLOUT什么时候被触发,就需要我们来看看什么时候socket可写: