使用套接字编程的服务器模型主要包括:循环(轮询)服务器模型、并发服务器模型和IO复用服务器模型。
套接字函数错误处理
在前几篇文章中的程序中,没有对程序进行出错处理,这是一个非常不好的习惯,在这里我们将出错函数进行了封装。下面直接使用封装之后的函数就好了。
#ifndef SOCKETERRORHANDING
#define SOCKETERRORHANDING
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <stddef.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Exit(const char* errStr);
int Socket(int domain,int type,int protocol);
int Bind(int sockfd,const struct sockaddr *my_addr,socklen_t addrlen);
int Listen(int sockfd,int backlog);
int Accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
int Connect(int sockfd,struct sockaddr *my_addr,int addrlen);
ssize_t Read(int sockfd,void* buf,size_t bufLen);
ssize_t Readn(int sockfd,void *buf ,size_t bufLen);
ssize_t Write(int sockfd,const void* buf,size_t bufLen);
ssize_t Writen(int sockfd,const void* buf,size_t bufLen);
int Close(int sockfd);
#endif
#include "sockErrHand.h"
void Exit(const char* errStr)
{
perror(errStr);
}
int Socket(int domain,int type,int protocol)
{
int sockDis=socket(domain,type,protocol);
if(sockDis<0)
{
Exit("socket error!");
}
return sockDis;
}
int Bind(int sockfd,const struct sockaddr* my_addr,socklen_t addrlen)
{
int n=bind(sockfd,my_addr,addrlen);
if(n<0)
{
Exit("bind error");
}
return 0;
}
int Listen(int sockfd,int backlog)
{
if(listen(sockfd,backlog)<0)
{
Exit("listen error");
}
return 0;
}
int Accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
{
int newSockfd=accept(sockfd,addr,addrlen);
while(newSockfd<0&&(errno == ECONNABORTED || errno == EINTR))
{
sleep(1);
sockfd=accept(sockfd,addr,addrlen);
}
if(newSockfd<0)
{
Exit("accept error");
}
return newSockfd;
}
int Connect(int sockfd,struct sockaddr *my_addr,int addrlen)
{
if(connect(sockfd,my_addr,addrlen)<0)
{
Exit("connect error");
}
return 0;
}
ssize_t Read(int sockfd,void *buf,size_t bufLen)
{
ssize_t n=read(sockfd,buf,bufLen);
while(n<0&&errno==EINTR)
{
n=read(sockfd,buf,bufLen);
}
if(n<0)
{
Exit("read error");
}
return n;
}
ssize_t Write(int sockfd,const void* buf,size_t bufLen)
{
ssize_t n=write(sockfd,buf,bufLen);
while(n<0&&errno==EINTR)
{
n=write(sockfd,buf,bufLen);
}
if(n<0)
{
Exit("write error");
}
return n;
}
int Close(int sockfd)
{
if(close(sockfd)<0)
{
Exit("close error");
}
return 0;
}
ssize_t Readn(int sockfd,void *buf ,size_t bufLen)
{
//当接收的数据包大于MTU时,会将数据分割为几个包。
ssize_t rlen=0,len;
while(rlen<bufLen)
{
len=Read(sockfd,buf,bufLen);
rlen+=len;
buf+=rlen;
}
return rlen;
}
ssize_t Writen(int sockfd,const void* buf,size_t bufLen)
{
ssize_t rlen=0,len;
while(rlen<bufLen)
{
len=Write(sockfd,buf,bufLen);
rlen+=len;
buf+=rlen;
}
return rlen;
}
循环模型
循环服务器指的是对于客户端的请求和连接,服务器再处理完毕一个之后再处理另一个,即进行串行处理客户端的请求。循环服务器经常用于UDP服务程序,例如时间服务程序、DHCP服务器。
UDP循环服务器模型
服务器再recv和处理数据这两项业务之间轮询处理,所以循环服务器模型又被称为轮询服务器模型。
程序1、UDP循环模型服务器
服务器功能:客服端发送时间请求,服务器向客户端会送服务器端的时间。
#include <string.h>
#include <stdio.h>
#include <time.h>
#include "sockErrHand.h"
#define PORT 5500
#define SIZE 1024
int main()
{
int serSock=Socket(AF_INET,SOCK_DGRAM,0);
struct sockaddr_in serAddr,cliAddr;
bzero(&serAddr,sizeof(serAddr));
bzero(&cliAddr,sizeof(cliAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serAddr.sin_port=htons(PORT);
Bind(serSock,(struct sockaddr*)(&serAddr),sizeof(serAddr));
printf("套接字和端口成功已绑定\n");
while(1)
{
char dataBuf[SIZE];
time_t t;
socklen_t cliAddrLen=sizeof(cliAddr);
//清空数据区内容,防止发送内容对接收内容造成影响
memset(dataBuf,0,SIZE);
recvfrom(serSock,(void*)(dataBuf),SIZE,0,(struct sockaddr*)(&cliAddr),&cliAddrLen);
//当发送的数据为TIME是处理,否则不处理
if(strcmp("TIME\n",dataBuf)==0)
{
time(&t);
//防止接收内容对发送内容造成影响
memset(dataBuf,0,SIZE);
ctime_r(&t,dataBuf);
sendto(serSock,(void*)dataBuf,strlen(dataBuf),0,(struct sockaddr*)(&cliAddr),cliAddrLen);
}
}
Close(serSock);
return 0;
}
使用nc命令测试服务器:
程序二、TCP循环服务器模型
相比较于UDP协议的循环服务器,TCP协议的循环服务器的主过程中多了一个accept的过程,即TCP服务器需要和客户端建立连接之后才可以通信,而且accept函数为阻塞函数,所以一般情况下,服务器会阻塞再accept函数出等待客户端的连接。对accept函数的不同处理时区分各种服务求的一个重要依据。
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "sockErrHand.h"
#define PORT 5500
#define LISNUM 64
#define SIZE 1024
int main()
{
//ipv4、流式套接字和默认协议
int serSock=Socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serAddr,cliAddr;
bzero(&serAddr,sizeof(serAddr));
bzero(&cliAddr,sizeof(cliAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_addr.s_addr=htonl(INADDR_ANY);
serAddr.sin_port=htons(PORT);
Bind(serSock,(struct sockaddr_in*)(&serAddr),sizeof(serAddr));
Listen(serSock,LISNUM);
printf("TCPSERVERLISTENING.....\n");
while(1)
{
char dataBuf[SIZE];
time_t t;
socklen_t cliAddrLen=sizeof(cliAddr);
memset(dataBuf,0,SIZE);
int cliSock=Accept(serSock,(struct sockaddr*)(&cliAddr),&cliAddrLen);
int readLen=Read(cliSock,(void*)dataBuf,SIZE);
if(strcmp("TIME\n",dataBuf)==0)
{
memset(dataBuf,0,SIZE);
time(&t);
ctime_r(&t,dataBuf);
Write(cliSock,(void*)dataBuf,strlen(dataBuf));
}
Close(cliSock);
}
Close(serSock);
return 0;
}
在上面的程序中存在一些问题:
因为accept、read和write函数都是阻塞函数,所以在程序执行过程中就有可能发生阻塞。
(1)如果没有客户端的连接请求,进程(单进程)会阻塞在accept函数出,程序不能进行其他任何操作(系统调用使得程序从用户态进入了内存态)。
(2)当和客户端成功建立连接后,程序会阻塞在read处,等待接收客户端发送的数据,如果此时有其他客户端尝试建立连接,程序就会无法响应。
(3)如果客户端接收数据异常的缓慢(客户端读取文件的长度比服务器写文件的速度慢),write就会导致写缓冲区满,数据发送不出去。
如果将这些阻塞函数修改为非阻塞函数:
那么就需要服务器就会一直在accept、read和write三个函数之间不停的循环,一旦客户端连接过多就会导致服务器的压力增大。