Accept + read/write ,一次服务一个客户,迭代TCP服务器
总是在处理完某个客户的请求之后才转向下一个客户。不是并发服务器。
本文利用c++在Linux编写了一个echo程序,实现回显服务,把服务端将收到的数据发回客户端,主要用到的技术有Reactor 模式,socket,非阻塞IO,进程,迭代服务器编程等知识。
将客户端发送给服务器的内容进行 ROT13加密 之后发回客户端。
采用IO复用 + 非阻塞IO模型(Reactor 模式),其中IO复用采用的是Linux下的epoll机制,下面首先介绍Linux下的5中IO模型。
IO复用的是线程,不是IO连接
综上所述,阻塞和非阻塞的区别是:调用进程是否立刻返回;同步和异步的区别是:将数据从内核空间复制到用户空间时,调用进程是否阻塞。
这里select手册给出了答案
Under Linux, select() may report a socket file descriptor as “ready for reading”, while nevertheless a subsequent read blocks. This could for example happen when data has arrived but upon examination has wrong checksum and is discarded. There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.
这里的意思就是说,如果某个socket接收缓冲区有新数据分节到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误,然后丢弃这个分节,这时候调用read则无数据可读,如果socket没有被设置nonblocking,此read将阻塞当前线程。如果socket被设置为非阻塞的话,没有数据可读时,就会返回一个错误。
select只支持水平触发,epoll支持水平触发和边沿触发两种模式,默认模式为水平触发。那么水平触发和边沿触发的区别是什么呢?
水平触发:只要满足条件,就触发事件(数据未读完,内核会一直通知你)。
边沿触发:每当状态变化时,触发事件。
边沿触发存在一个问题:read一个文件描述符时,一定要将buffer中的数据全部读完,也就是反复用read(),直至其遇到EAGAIN或者read返回值为0 为止;如果没有读完,则系统互认为该文件描述符状态没有变化,就不会再通知此文件描述符了,此时这个文件描述符就像dead一样。
epoll三要素:红黑树、mmap和链表。
epoll内部实现是通过内核与用户空间mmap一块内存实现的,mmap将用户空间的一块地址和内核空间额度一块地址映射到相同的一块物理内存地址,减少了内核态和用户态之间的数据交换。
红黑树将存储epoll所监听的套接字,添加和删除socket时性能好。
在默认情况下,当监听服务器重启时,通过调用socket、bind、listen重新启动时,由于它试图捆绑一个现有连接(之前派生的子进程处理着的连接)上的端口,从而bind调用会失败。
解决办法: 在socket 和bind 两个调用之间设置了SO_REUSEADDR套接字选项,则此时bind就会调用成功。 (所有TCP服务器都应该指定本套接字选项,以允许服务器在此情形下被重新启动)
这种情形意味着本进程的文件描述符已经到达了上限,无法为新连接创建socket文件描述符,但是由于此”连接“ 并没有获得文件描述符,我们就无法close(),程序继续运行,epoll_wait()会立刻返回,因为新连接待处理,listening fd 还是可读的,这样互导致服务端程序陷入busy loop,影响其他连接的正常运行。
解决办法: 由于文件描述符是hard limit,我们可以自己设置一个稍低一点的soft limit ,如果连接数超过了这个soft limit, 就主动关闭新连接。
使用非阻塞IO时,通常将应用程序任务划分到多个进程(使用fork)或多个线程,这里我使用的是将当前进程划分成两个子进程,其中子进程用来将来自服务器的消息复制到标准输出,父进程将来自客户端标准输入的消息复制给服务器,如图所示。父子进程共享同一个套接字:父进程往套接字里面写,子进程从套接字里读,两个文件描述符在引用同一个套接字
######## echo.h ############
#ifndef _ECHO_
#define _ECHO_
#include //常用数据类型定义头文件
#include
#include
#include
#include
#include //地址转换
#include //提供对操作系统应用接口访问功能的头文件 fork 等、、、
#include //常用的系统函数,free、、、
#include
#include
#include
using namespace std;
#define MAX_EVENT_NUMBER 1024
#define BUF_SIZE 300
#define EPOLL_SIZE 100 //epoll最大监听数
#define PORT "2019" //atoi(PORT),将字符串变为整形
#define SERVERIP "127.0.0.1"
int setNonblocking(int sockfd){
int flags = fcntl(sockfd,F_GETFL);//获取旧的文件标志
int newflags = flags | O_NONBLOCK;
int n;
if((n = fcntl(sockfd,newflags)) == -1){
perror("fcntl error");
exit(-1);}
return flags;
}
void addfd(int epollfd,int sockfd,int state){
struct epoll_event event;
event.data.fd = sockfd;
event.events = state;
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);
}
void handleEvent(struct epoll_event * EventLoop, int eventsNumber,int epollfd,int listenfd){
int confd = 0;
struct sockaddr_in clientaddr;
char buf[BUF_SIZE];
for(int i= 0; i< eventsNumber;++i){
if(EventLoop[i].data.fd == listenfd){ //如果是第一次建立连接???这里有问题,待改善
socklen_t clientlength = sizeof(clientaddr);
confd = accept(listenfd,(struct sockaddr*) &clientaddr,&clientlength);
addfd(epollfd,confd,EPOLLIN);
//clients.push_back(clientsocket);
}
else{
confd = EventLoop[i].data.fd;
bzero(&buf,strlen(buf));
int len = sprintf(buf,"After RTO :");
int nread = 0;
if((nread = read(confd,buf+len,BUF_SIZE))<0){
perror("read error");
exit(-1);}
for(unsigned int i = nread;i < strlen(buf);++i){ //ROT 加密
if((buf[i] >= 'a' && buf[i] <= 'm') || (buf[i] >= 'A' && buf[i] <= 'M')){
buf[i] = char(buf[i] + 13);
continue;}
else if((buf[i] >= 'm' && buf[i] <= 'z') || (buf[i] >= 'M' && buf[i] <= 'Z')){
buf[i] = char(buf[i] - 13);
continue;}
}
buf[strlen(buf)+1] = 0;
write(confd,buf,strlen(buf)+1);
}
}
}
#endif
########## echoserver.cpp #############
#include "echo.h"
int main(){
int error = 0;
char buf[BUF_SIZE];
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
inet_aton(SERVERIP,&serveraddr.sin_addr);
serveraddr.sin_port = htons(atoi(PORT));
int listenfd = -1;
if((listenfd = socket(PF_INET,SOCK_STREAM,0)) == -1){
perror("socket error");
exit(-1);
}
int optval = 0;
socklen_t optlen;
optlen = sizeof(optval);
optval = 1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,(void*) optval,optlen);
if((bind(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))) == -1){
perror("bind error");
exit(-1);
}
int listenopt = 10;
char * ptr;
if((ptr = getenv("LISTENQ")) != NULL)
listenopt = atoi(ptr);
if((listen(listenfd,listenopt)) == -1){
perror("listen error");
exit(-1);
}
int epollfd = epoll_create(EPOLL_SIZE); //监听的数目
struct epoll_event EventLoop[MAX_EVENT_NUMBER];
addfd(epollfd,listenfd,EPOLLIN);//添加监听事件,调用epoll_ctl
int eventsNumber = 0;
while(1){
eventsNumber = epoll_wait(epollfd,EventLoop,EPOLL_SIZE,-1); //-1表示阻塞
if(eventsNumber == -1){
perror("epollwait error");
exit(-1);
}
handleEvent(EventLoop,eventsNumber,epollfd,listenfd);
}
close(listenfd);
close(epollfd);
}
################ echocli.cpp #######################
#include "echo.h"
void readfrom(int clientsocket,char * buf);
void writeto(int clientsocket,char * buf);
int main(){
int clientsocket;
char buf[BUF_SIZE];
if((clientsocket = socket(PF_INET,SOCK_STREAM,0))== -1){
perror("client socket error");
exit(-1);
}
struct sockaddr_in serveraddr;
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
inet_aton(SERVERIP,&serveraddr.sin_addr);
serveraddr.sin_port = htons(atoi(PORT));
if((connect(clientsocket,(struct sockaddr *) & serveraddr,sizeof(serveraddr))) == -1){
perror("connect error");
exit(-1);
}
pid_t pid = fork();
if(pid == 0){
writeto(clientsocket,buf);
}
else{
readfrom(clientsocket,buf);
}
close(clientsocket);
return 0;
}
void readfrom(int clientsocket,char * buf){
while(1){
if((read(clientsocket,buf,BUF_SIZE)) == 0){
perror("client read error");
exit(-1);
}
printf("%s",buf);
}
}
void writeto(int clientsocket,char * buf){
while(1){
fgets(buf,BUF_SIZE,stdin);
if(!strcmp(buf,"exit\n")){
shutdown(clientsocket,SHUT_WR);
return;
}
write(clientsocket,buf,strlen(buf));
}
}
本代码目前只能实现基本的echo功能,还不完善,后续会改进。
本文基于很多博主的博客,因为之前是抄写在笔记本上的,所以找不到链接,如果您觉得侵权了,请私信我,我会在第一时间删除相应的内容。
分类 网络编程 学习笔记