TCP循环服务器的实现也不难:TCP服务器接受一个客户端的连接,然后处理,完成了这个客户的所有请求后,断开连接.
TCP循环服务器一次只能处理一个客户端的请求.只有在这个客户的所有请求都满足后, 服务器才可以继续后面的请求.这样如果有一个客户端占住服务器不放时,其它的客户机都不能工作了.因此,TCP服务器一般很少用循环服务器模型的.
3.并发服务器:TCP服务器
为了弥补循环TCP服务器的缺陷,人们又想出了并发服务器的模型. 并发服务器的思想是每一个客户机的请求并不由服务器直接处理,而是服务器创建一个 子进程来处理.
算法如下:
socket(...);
bind(...);
listen(...);
while(1)
{
accept(...);
if(fork(..)==0)
{
while(1)
{
read(...);
process(...);
write(...);
}
close(...);
exit(...);
}
close(...);
}
TCP并发服务器可以解决TCP循环服务器客户机独占服务器的情况. 不过也同时带来了一个不小的问题.为了响应客户机的请求,服务器要创建子进程来处理. 而创建子进程是一种非常消耗资源的操作.
实例:
//fork()多个进程处理多个客户机的连接
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define BACKLOG 2
#define MAXDATASIZE 1000
void process_cli(int connectfd,struct sockaddr_in client);
int main(void)
{
int listenfd,connectfd;
pid_t pid;
struct sockaddr_in server,client;
int sin_size;
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1) //创建socket
{
printf("create socket error.\n");
exit(1);
}
int opt = SO_REUSEADDR;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//设置socket状态,函数原型:int setsockopt(int s,int level,int optname,const void *optval,socklen_t optlen)
//setsockopt()用来设置参数s所指定的socket状态。参数level代表欲设置的网络层,一般设为SOL_SOCKET以存取socket层,
//它有三种值:1、SOL_SOCKET:通用套接字选项;2、IPPROTO_IP:IP选项;3、IPPROTO_TCP:TCP选项
//参数optname代表获得或设置套接字选项;optval代表欲设置的值;参数optlen为optval的长度
bzero(&server,sizeof(server)); //置0
server.sin_family = AF_INET; //设置地址簇为internet地址簇
server.sin_port = htons(PORT); //设置端口号并(把主机字节顺序)转化为网络字节顺序
server.sin_addr.s_addr = htonl(INADDR_ANY); //设置IP地址
if(bind(listenfd,(struct sockaddr *)&server,sizeof(struct sockaddr)) == -1) //把创建的socket与服务器绑定
{
printf("bind error.\n");
exit(1);
}
if(listen(listenfd,BACKLOG) == -1) //socket进入监听,BACKLOG为请求队列的最大允许数
{
printf("listen error.\n");
exit(1);
}
sin_size = sizeof(struct sockaddr_in);
while(1)
{
if((connectfd = accept(listenfd,(struct sockaddr *)&client,&sin_size)) == -1) //与客户机建立连接
{
printf("accept error.\n");
exit(1);
}
pid = fork(); //创建子进程,用来处理与客户机的通信
if(pid > 0) //在父进程中
{
close(connectfd);
continue;
}
else if(pid == 0) //在子进程中
{
close(listenfd);
process_cli(connectfd,client); //与客户机通信
exit(0);
}
else
{
printf("fork error.\n");
exit(0);
}
}
close(listenfd);
}
void process_cli(int connectfd,struct sockaddr_in client)
{
int num;
char recvbuf[MAXDATASIZE+1],sendbuf[MAXDATASIZE+1],cli_name[MAXDATASIZE+1];
printf("You got a connection from %s.\n",inet_ntoa(client.sin_addr));
num = recv(connectfd,cli_name,MAXDATASIZE,0); //接受客户机传来的客户机名
//因为客户机是用fgets()发送,此函数会自动在最后一个字符后加上'\0',
if(num == 0)
{
close(connectfd);
printf("client disconnectde.\n");
return;
}
printf("client,s name is %s\n",cli_name);
while(num = recv(connectfd,recvbuf,MAXDATASIZE,0)) //接受客户机传来的数据
{
recvbuf[num] = '\0'; // 在末尾加'\0'是防止recvbuf中原本就存在数据,这样就不会把多余的数据打印出来
printf("recerved client(%s) message:\t%s\n",cli_name,recvbuf);
int i;
for(i = 0;i < num - 1;i++)
{
sendbuf[i] = recvbuf[num - i - 2]; //倒置
}
//sendbuf[num - 1] = '\0'; //可不用这条语句,存入内存系统会自动加'\0',传送给客户机时会取'\0'再传送
//由于'\0'不传送,所以客户机的recv()要在末尾加'\0'再打印出去,以免打印出多余数据
send(connectfd,sendbuf,strlen(sendbuf),0);
}
close(connectfd);
}
当然也可以用多线程,因为多进程非常耗资源,创建的子进程太多会使系统崩毁。
多线程实例:
//多线程处理,服务器端
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define BACKLOG 5
#define MAXDATASIZE 1000
void process_cli(int connectfd,struct sockaddr_in client);
void *start_routine(void *arg);
struct ARG
{
int connfd;
struct sockaddr_in client;
};
int main(void)
{
int listenfd,connectfd;
pthread_t thread;
struct ARG *arg;
struct sockaddr_in server,client;
int sin_size;
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
printf("create socket error.\n");
exit(1);
}
int opt = SO_REUSEADDR;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//设置socket状态
bzero(&server,sizeof(server)); //置0,用于存放服务器地址
server.sin_family = AF_INET; //设置server地址中的协议簇,为internet
server.sin_port = htons(PORT); //设置server地址中的端口号,并转化为网络字符字节
server.sin_addr.s_addr = htonl(INADDR_ANY); //设置IP为自动获取,INZDDR_ANY可改为确定的IP地址
if(bind(listenfd,(struct sockaddr *)&server,sizeof(struct sockaddr)) == -1)
//socket与服务器绑定
{
printf("bind error.\n");
exit(1);
}
if(listen(listenfd,BACKLOG) == -1) //socket监听
{
printf("listen error.\n");
exit(1);
}
sin_size = sizeof(struct sockaddr_in);
while(1) //不断进行监听与连接
{
if((connectfd = accept(listenfd,(struct sockaddr *)&client,&sin_size)) == -1)
//与客户机连接
{
printf("accept error.\n");
exit(1);
}
arg = malloc(sizeof(struct ARG));//分配内存,函数原型:void *malloc(size_t size)
arg->connfd = connectfd;
memcpy((void *)&arg->client,&client,sizeof(client));
//拷贝内存空间,函数原型:void *memcpy(void *dest,const void *src,size_t n)
//表示从src所指的内存空间区域拷贝n个字节到dest所指内存区域
if(pthread_create(&thread,NULL,start_routine,(void *)arg))
//创建线程,函数原型:int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),(void *)arg);
//thread为线程标识符;attr为线程属性设置(NULL为采用默认值);start_routine为线程函数的启示地址,arg为传递给start_routine()的参数
//这语句的意思是从函数start_routine地址处创建线程,也就是把函数start_routine()作为线程来执行
{
printf("pthread create error.\n");
exit(1);
}
}
close(listenfd);
}
void process_cli(int connectfd,struct sockaddr_in client) //自定义函数用于处理与客户机的通信
{
int num;
char recvbuf[MAXDATASIZE+1],sendbuf[MAXDATASIZE+1],cli_name[MAXDATASIZE+1];
printf("you got a connection from %s\n",inet_ntoa(client.sin_addr));
num = recv(connectfd,cli_name,MAXDATASIZE,0);
if(num == 0)
{
close(connectfd);
printf("client disconnected.\n");
return;
}
//cli_name[num - 1] = '\0'; //这句的作用是把从客户机传来的数据的最后一个字符(即回车符)去掉
//因为客户端是用fgets()接收并传数据,fgets()会接收回车符,这样输出的
//的数据中在就不会在客户机的名字后换行(即106那行语句的输出)
printf("client name is %s\n",cli_name);
while(num = recv(connectfd,recvbuf,MAXDATASIZE,0)) //接收到数据时
{
recvbuf[num] = '\0';
printf("received client (%s) message:%s\n",cli_name,recvbuf);
int i;
for(i = 0;i < num - 1;i++)
{
sendbuf[i] = recvbuf[num - i - 2]; //倒置
}
//sendbuf[num - 1] ='\0'; //可不要,系统会自动加上
send(connectfd,sendbuf,strlen(sendbuf),0); //发送sendbuf中的数据到套接字connectfd
}
close(connectfd);
}
void *start_routine(void *arg)
{
struct ARG *info;
info = (struct ARG *)arg;
process_cli(info->connfd,info->client);
free(arg); //释放内存空间,对应前面的语句arg=malloc(sizeof(struct ARG));
pthread_exit(NULL); //调用此函数主动退出线程
}
4.并发服务器:多路复用I/O
为了解决创建子进程带来的系统资源消耗,人们又想出了多路复用I/O模型.
首先介绍一个函数select
int select(int nfds,fd_set *readfds,fd_set *writefds,
fd_set *except fds,struct timeval *timeout)
void FD_SET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)
void FD_ZERO(fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)
一般的来说当我们在向文件读写时,进程有可能在读写出阻塞,直到一定的条件满足. 比如我们从一个套接字读数据时,可能缓冲区里面没有数据可读(通信的对方还没有 发送数据过来),这个时候我们的读调用就会等待(阻塞)直到有数据可读.如果我们不 希望阻塞,我们的一个选择是用select系统调用. 只要我们设置好select的各个参数,那么当文件可以读写的时候select回"通知"我们 说可以读写了. readfds所有要读的文件文件描述符的集合
writefds所有要的写文件文件描述符的集合
exceptfds其他的服要向我们通知的文件描述符
timeout超时设置.
nfds所有我们监控的文件描述符中最大的那一个加1
在我们调用select时进程会一直阻塞直到以下的一种情况发生. 1)有文件可以读.2)有文件可以写.3)超时所设置的时间到.
为了设置文件描述符我们要使用几个宏. FD_SET将fd加入到fdset
FD_CLR将fd从fdset里面清除
FD_ZERO从fdset中清除所有的文件描述符
FD_ISSET判断fd是否在fdset集合中
使用select后我们的服务器程序就变成了.
初始话(socket,bind,listen);
while(1)
{
设置监听读写文件描述符(FD_*);
调用select;
如果是倾听套接字就绪,说明一个新的连接请求建立
{
建立连接(accept);
加入到监听文件描述符中去;
}
否则说明是一个已经连接过的描述符
{
进行操作(read或者write);
}
}
多路复用I/O可以解决资源限制的问题.着模型实际上是将UDP循环模型用在了TCP上面. 这也就带来了一些问题.如由于服务器依次处理客户的请求,所以可能会导致有的客户 会等待很久.
实例:
//使用select()实现
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define BACKLOG 5
#define MAXDATASIZE 1000
typedef struct CLIENT //定义结构体用于保存客户机信息与传来的数据
{
int fd;
char *name;
struct sockaddr_in addr;
char *data;
};
void process_cli(struct CLIENT *client,char *recvbuf,int len);
void savedata(char *recvbuf,int len,char *data);
int main(void)
{
int i,maxi,maxfd,sockfd;
int nready;
ssize_t n;
fd_set rset,allset; //文件描述符集合
int listenfd,connectfd;
struct sockaddr_in server;
struct CLIENT client[FD_SETSIZE];
char recvbuf[MAXDATASIZE];
int sin_size;
if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1) //创建socket
{
printf("create socket error.\n");
exit(1);
}
int opt = SO_REUSEADDR;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); //设置socket状态
bzero(&server,sizeof(server)); //清0
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取IP
if(bind(listenfd,(struct sockaddr *)&server,sizeof(struct sockaddr)) == -1)
//服务器socket与地址绑定
{
printf("bind error.\n");
exit(1);
}
if(listen(listenfd,BACKLOG) == -1) //socket监听
{
printf("listen error.\n");
exit(1);
}
sin_size = sizeof(struct sockaddr_in);
maxfd = listenfd; //maxfd存放最大的文件描述符
maxi = -1;
for(i = 0;i < FD_SETSIZE;i++) //初始化文件描述符为-1
{
client[i].fd = -1;
}
FD_ZERO(&allset); //文件描述符集合allset清零
FD_SET(listenfd,&allset); //把listenfd加入文件描述符集合allset中
while(1)
{
struct sockaddr_in addr;
rset = allset; //rset文件描述符集合中的文件由select监视是否可被读取
nready = select(maxfd+1,&rset,NULL,NULL,NULL);
if(FD_ISSET(listenfd,&rset))
//测试listenfd是否存在于rset中,若在
{
if((connectfd = accept(listenfd,(struct sockaddr *)&addr,&sin_size)) == -1) //接收
{
printf("accept error.\n");
continue;
}
for(i = 0;i < FD_SETSIZE;i++)
{
if(client[i].fd < 0)
{
char *name;
char *data;
name = malloc(sizeof(char[MAXDATASIZE]));
data = malloc(sizeof(char[MAXDATASIZE]));
client[i].fd = connectfd;
client[i].name = name;
client[i].addr = addr;
client[i].data = data;
client[i].name[0] = '\0';
client[i].data[0] = '\0';
printf("You got a connect from %s \n",inet_ntoa(client[i].addr.sin_addr));
break;
}
}
if(i == FD_SETSIZE)
{
printf("to many clients\n");
}
FD_SET(connectfd,&allset); //把套接字connectfd加入文件描述符集合allset中
if(connectfd > maxfd)
{
maxfd = connectfd; //因为maxfd是用于存放最大的文件描述符的
}
if(i > maxi)
{
maxi = i;
}
if(--nready <= 0)
{
continue;
}
}
for(i = 0;i <= maxi;i++)
{
if((sockfd = client[i].fd) < 0)
{
continue;
}
if((n = recv(sockfd,recvbuf,MAXDATASIZE,0)) == 0)
{
close(sockfd);
printf("client(%s) closed connection .User's data: %s\n",client[i].name,client[i].name,client[i].data);
FD_CLR(sockfd,&allset); //将套接字sockfd从文件描述符集合allset中删除
client[i].fd = -1;
}
else
{
process_cli(&client[i],recvbuf,n);
}
if(--nready <= 0)
{
break;
}
}
}
close(listenfd);
}
void process_cli(struct CLIENT *client,char *recvbuf,int len)
{
char sendbuf[MAXDATASIZE];
recvbuf[len - 1] == '\0';
if(strlen(client->name) == 0)
{
memcpy(client->name,recvbuf,len);//从recvbuf中拷贝len字节数据到client->name中
printf("client's name is %s\n",client->name);
return;
}
printf("Received client (%s) message:\t%s\n",client->name,recvbuf);
savedata(recvbuf,len,client->data); //调用savedata函数
int i1;
for(i1 = 0;i1 < len - 1;i1++)
{
sendbuf[i1] = recvbuf[len - i1 -2];
}
//sendbuf[len - 1] = '\0';
send(client->fd,sendbuf,strlen(sendbuf),0);
}
void savedata(char *recvbuf,int len,char *data)
{
int start = strlen(data);
int i;
for(i = 0;i < len;i++)
{
data[start + i] = recvbuf[i];
}
}
5.并发服务器:UDP服务器
人们把并发的概念用于UDP就得到了并发UDP服务器模型. 并发UDP服务器模型其实是简单的.和并发的TCP服务器模型一样是创建一个子进程来处理的 算法和并发的TCP模型一样.
除非服务器在处理客户端的请求所用的时间比较长以外,人们实际上很少用这种模型.
6.客户端:UDP
上面的都是在说服务器端,为了调试,现在就给两个客户端的程序,当然客户端很简单了,我不想多说,直接看程序吧。
实例:
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define MAXDATASIZE 100
int main(int argc, char *argv[])
{
int fd,numbytes;
char buf[MAXDATASIZE+1];
struct hostent *he;
struct sockaddr_in server,reply;
if(argc != 3)
{
printf("Usage:%s \n");
exit(1);
}
if((he = gethostbyname(argv[1])) == NULL) //把主机名或IPv4或IPv6转化为hostent结构的地址
{
printf("gethostbyname error.\n");
exit(1);
}
if((fd = socket(AF_INET,SOCK_DGRAM,0)) == -1)
{
printf("socket error.\n");
exit(1);
}
bzero(&server,sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr); //in_addr为32位IPv4的地址结构
sendto(fd,argv[2],strlen(argv[2]),0,(struct sockaddr *)&server,sizeof(struct sockaddr)); //发送argv[2]中的数据到套接字fd,此fd为与server通信的通道
while(1)
{
int len;
if((numbytes = recvfrom(fd,buf,MAXDATASIZE,0,(struct sockaddr *)&reply,&len)) == -1)
{
printf("recvfrom error.\n");
exit(1);
}
if(len != sizeof(struct sockaddr) || memcmp((const void *)&server,(const void *)&reply,len) != 0)
//当接受到的数据的地址reply与服务器的地址不相同时,说明该数据是来自别的服务器的
{
printf("recerve message from other server.\n");
continue;
}
buf[numbytes] = '\0';
printf("server message:%s\n",buf);
break;
}
close(fd);
}
7.客户端:TCP
实例:
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define MAXDATASIZE 100
int main(int argc,char *argv[])
{
int fd,numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in server;
if(argc != 2)
{
printf("Usage:%s \n",argv[0]);
exit(1);
}
if((he = gethostbyname(argv[1])) == NULL) //调用gethostbyname()进行地址转化,转化为hostent结构类型值,参数argc[1]可以为IPv4、IPv6地址或主机名.需要头文件为#include
{
printf("gethostbyname() error.\n");
exit(1);
}
if((fd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建socket套接字,返回的套接字文件描述符赋给fd;出错判断
{
printf("socket() error.\n");
exit(1);
}
bzero(&server,sizeof(server));//需要头文件#include .函数原型void bzero(void *s,size_t n);功能是把地址s所指内容中的前n个字节区域置零
server.sin_family = AF_INET;
server.sin_port = htons(PORT);
server.sin_addr = *((struct in_addr *)he->h_addr);
//根据gethostbyname()返回值he进行服务器地址结构体赋值转化,用于进行网络连接,使之能与服务器建立连接
if(connect(fd,(struct sockaddr *)&server,sizeof(struct sockaddr)) == -1)//把服务器地址类型转化为内核所需类型,进行服务器连接
{
printf("connect() error\n");
exit(1);
}
if((numbytes = recv(fd,buf,MAXDATASIZE,0)) == -1)//接收套接字fd中的数据,放入缓存buf中。函数原型ssize_t recv(int s,void *buf,size_t len,int flags),用于面向连接的socket
{
printf("revc() error\n");
exit(1);
}
buf[numbytes] = "\0";
printf("Server Message:%s\n ",buf);
close(fd);
}
8.客户端:并发
实例:
//多进程处理连接
#include
#include
#include
#include
#include
#include
#include
#define PORT 1234
#define MAXDATASIZE 100
void process(FILE *fp,int sockfd);
char *getmessage(char *sendline,int len,FILE *fp);
int main(int argc,char **argv)
{
int fd;
struct hostent *he;
struct sockaddr_in server;
if(argc != 2)
{
printf("Usage: %s \n",argv[0]);
exit(1);
}
if((he = gethostbyname(argv[1])) == NULL) //把主机名或IPv4或IPv6转化为hostent结构的地址
{
printf("gethostbyname error.\n");
exit(1);
}
if((fd = socket(AF_INET,SOCK_STREAM,0)) == -1) //创建socket
{
printf("socket error.\n");
exit(1);
}
bzero(&server,sizeof(server));//置0
server.sin_family = AF_INET; //设置为internet协议簇
server.sin_port = htons(PORT);//设置端口号
server.sin_addr = *((struct in_addr *)he->h_addr);//把hostent结构的地址转化为IPv4地址结构并赋值
if(connect(fd,(struct sockaddr *)&server,sizeof(struct sockaddr)) == -1)
//与服务器连接
{
printf("connect error.\n");
exit(1);
}
process(stdin,fd);
close(fd);
}
void process(FILE *fp,int sockfd)
{
char sendline[MAXDATASIZE+1],recvline[MAXDATASIZE+1];
int numbytes;
printf("connected to server.\n");
printf("Input name:");
if(fgets(sendline,MAXDATASIZE,fp) == NULL) //把fp文件中的数据输入到sendline中,由主函数调用可知fp为标准输入(stdio)
//fgets()回车也会接收并存到sendline中,并且会在最后一个字符后加'\0'
{
printf("exit.\n");
return;
}
send(sockfd,sendline,strlen(sendline),0); //把sendline中的数据发送到套接字sockfd
while(getmessage(sendline,MAXDATASIZE,fp) != NULL) //定义接收输入数据的自定义函数
{
send(sockfd,sendline,strlen(sendline),0);//可在此处加上语句bzero(recvline,MAXDATASIZE+1);这样可不要第74行(加'\0'那行)语句
if((numbytes = recv(sockfd,recvline,MAXDATASIZE,0)) == 0)
{
printf("server terminated.\n");
return;
}
recvline[numbytes] = '\0';//在末尾加'\0'再打印,防止recvline内存空间原本就有内容,否则会打印出多余数据
printf("server message:\t%s\n",recvline);
}
printf("exit.\n");
}
char *getmessage(char *sendline,int len,FILE *fp)
{
printf("Input string to server\n");
return(fgets(sendline,MAXDATASIZE,fp)); //嵌套调用
}