总体来说,服务器的运行模式大体有两类:循环服务器和并发服务器。所谓的循环服务器就是说他给客户端提供的服务时一个接着一个的,不能同时服务,也就是说当一个用户使用服务器的时候,其他用户不没能使用只能等待。这显然不符合实际服务器的要求,而并发服务器就可以很好地解决这个问题。并发服务器能够同时为多个客户端服务,同时并发服务的能力是服务器性能的一个重要指标。
并发服务器的实现总体有以下几种方法:
① 服务器和每个接收到的客户机进行连接,创建一个新的子进程处理这个客户机请求。
② 服务器预先创建多个子进程,由子进程处理客户机请求。这种方式叫做“预创建”服务器。
③ 服务器用函数select实现对多个客户机连接的多路复用。
④ 超级服务器激活的服务器
这里我们主要讨论第一种实现,我们分为两种实现方式:利用子进程实现,利用线程的方式实现。这里服务器端的功能就是接受客户端的数据,并在服务器屏幕上打印出来,而且要求客户机可以主动断开链接,服务器能够为多个客户机服务。
利用子进程方式实现,服务器端代码:
/*server_fork.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
/*定义通讯端口*/
#define MYPORT 4000
/*定义服务器端等待队列长度*/
#define BACKLOG 10
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"
int main(void)
{
int sock_fd,new_fd;
int numbytes;
int n;
int sin_size;
char buf[MAXDATASIZE];
/*IP地址数据结构*/
struct sockaddr_in server_ip,client_ip;
/*创建套接字*/
if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
{
perror("socket");
exit(1);
}
/*设置协议族为IPv4,端口为自定义端口,地址为本地任意可用IP地址*/
server_ip.sin_family = AF_INET;
server_ip.sin_port = htons(MYPORT);
server_ip.sin_addr.s_addr = htonl(INADDR_ANY);
n = 1;
/*设置套接字选项,使其可以重复使用端口*/
setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
/*绑定地址和端口信息*/
if( bind(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
{
perror("bind");
exit(1);
}
/*监听来自客户端的链接,队列长度为BACKLOG*/
if( listen(sock_fd,BACKLOG) == -1 )
{
perror("listen");
exit(1);
}
/*主操作部分*/
while(1)
{
sin_size = sizeof(struct sockaddr);
memset(buf,0,sizeof(buf));
/*接受下一个客户端链接*/
new_fd = accept(sock_fd,(struct sockaddr *)&client_ip,&sin_size);
if( new_fd == -1 )
{
perror("accept");
exit(1);
}
/*创建子进程,并定义子进程中的操作*/
if( !fork() )
{
printf("server get connection from %s\n",inet_ntoa(client_ip.sin_addr));
while(1)
{
/*接收来自客户端的信息*/
if( (numbytes=recv(new_fd,(void *)buf,MAXDATASIZE,0)) == -1 )
{
printf("receive data error!\n");
continue;
}
/*判断是不是断开链接的信息,是则向客户端发送回应,并断开链接,否则打印信息*/
if( strcmp(FINISH,buf) )
{
buf[numbytes] == '\0';
printf("server: %s has been read!\n",buf);
}
else
{
printf("%s\n",buf);
/*发送回应*/
if( (numbytes=send(new_fd,"Connection closed!\n",MAXDATASIZE,0)) == -1 )
{
printf("close error!\n");
exit(1);
}
printf("Connection closed!\n");
close(new_fd);
exit(0);
}
}
}
close(new_fd);
}
close(sock_fd);
return 0;
}
客户机端代码:
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
/*定义通讯端口*/
#define MYPORT 4000
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"
int main(int argc,char *argv[])
{
int sock_fd;
int numbytes;
char buf[MAXDATASIZE];
struct sockaddr_in server_ip;
/*主机信息结构体*/
struct hostent *h;
if( argc != 2)
{
printf("Usage: %s hostip\n",argv[0]);
exit(0);
}
/*将第二个参数转换为网络字节序的IP地址*/
if( (h=gethostbyname(argv[1])) == NULL )
{
herror("gethostbyname");
exit(1);
}
if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
{
perror("socket");
exit(1);
}
server_ip.sin_family = AF_INET;
server_ip.sin_port = htons(MYPORT);
/*将刚才得到的IP地址填入*/
server_ip.sin_addr = *((struct in_addr *)h->h_addr);
/*与服务器建立链接*/
if( connect(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
{
printf("connection error!\n");
exit(1);
}
while(1)
{
memset(buf,0,sizeof(buf));
printf("Please input a string:");
scanf("%s",buf);
/*发送数据*/
if( (numbytes=send(sock_fd,buf,MAXDATASIZE,0)) == -1 )
{
printf("data send error!\n");
exit(1);
}
/*如果输入的时断开链接的信号,则要监听网络上来自服务器的回应*/
if( !strcmp(FINISH,buf) )
{
if( (numbytes=recv(sock_fd,(void *)buf,MAXDATASIZE,0)) == -1 )
{
printf("close error!\n");
exit(1);
}
printf("%s\n",buf);
close(sock_fd);
printf("Byebye!\n");
exit(0);
}
}
return 0;
}
当多个客户机链接到服务器的时候,服务器会创建多个子进程分别为其服务。随着计算机硬件的发展,具有多核心,快处理频率的CPU越来越普及,因此在应用程序中也因该加以使用。在计算机系统中有一个比进程个更小的调度单位,那就是线程,一个进程可以分为多个线程,多个线程共享一个进程的资源,同时线程也是CPU调度的最小单位,也可以将线程看作轻量级的进程。
在本例中客户端不变,要将服务器端修改。由于调用fork函数创建子进程的时候,会将父进程的数据COW(copy on write),基本上就是复制了一份,所以父子进程的资源使用基本独立。但是线程则不一样,线程之间的资源是共享的,所以这时候互斥同步的线程调度就很重要。在本程序中,定了一个二维数组作为缓冲区,存放接受的字符串,在有限的数量内,每个线程有自己的缓冲区,当缓冲区占满时就要阻塞后来的线程并列队。这个时候就需要一种方式调度好缓冲区的使用。我们要实现的缓冲区调度功能是:当缓冲区空闲时可以使用,当一个满时使用另一个,所有都满时从第一个开始一个一个往后排列阻塞,均衡负载,线程和缓冲区没有固定的对应关系,完全随机使用,那个空闲就使用那个,没有空闲就从第一个依次等待。同时还要定义信号量来实现缓冲区的互斥使用。服务器代码如下:
/*server_fork.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <semaphore.h>
/*定义通讯端口*/
#define MYPORT 4000
/*定义服务器端等待队列长度*/
#define BACKLOG 10
/*定义数据缓冲区大小*/
#define MAXDATASIZE 1024
/*定义服务器同时可以服务的客户端数,为了方便测试,这里设置为2*/
#define MAXTHREAD 2
/*定义客户端发给服务器端断开链接的信号*/
#define FINISH "close"
/*服务器套接字和线程套接字*/
int sock_fd,new_fd[MAXTHREAD];
int numbytes;
int n;
int sin_size;
/*定义标志缓冲区是否可用的标志*/
int flag[MAXTHREAD];
/*定义缓冲区,一共MAXTHREAD个,每个MAXDATASIZE字节*/
char buf[MAXTHREAD][MAXDATASIZE];
/*定义每个缓冲区的信号量,实现互斥操作*/
sem_t sem[MAXTHREAD];
/*线程号*/
pthread_t thread[MAXTHREAD];
/*IP地址数据结构*/
struct sockaddr_in server_ip,client_ip;
/*自定义函数,功能是找到合适的缓冲区来分配给线程使用,当缓冲区的标志为0时表示可用,则返回序号,否则不可用返回-1,进行其他处理*/
int find(void)
{
int i = 0;
for(i=0;i<MAXTHREAD;i++)
{
if( flag[i] == 0 )
return i;
}
return -1;
}
/*线程处理函数*/
void *func(void *arg)
{
int thrd_num = (int)arg;
/*执行P操作*/
sem_wait(&sem[thrd_num]);
/*打印链接信息*/
printf("server get connection from %s,thread %d.\n",inet_ntoa(client_ip.sin_addr),thrd_num);
while(1)
{
/*接收客户端信息*/
if( (numbytes=recv(new_fd[thrd_num],(void *)buf[thrd_num],MAXDATASIZE,0)) == -1 )
{
perror("recv");
pthread_exit(NULL);
}
/*判断客户端的信息是不是断开信号,是则断开链接,否则打印收到的信息*/
if( strcmp(FINISH,buf[thrd_num]) )
{
buf[thrd_num][numbytes] == '\0';
printf("server: In the thread %d,%s has been read!\n",thrd_num,buf[thrd_num]);
}
else
{
printf("%s\n",buf[thrd_num]);
if( (numbytes=send(new_fd[thrd_num],"Connection closed!\n",MAXDATASIZE,0)) == -1 )
{
printf("close error!\n");
pthread_exit(NULL);
}
printf("In the thread %d,connection closed!\n",thrd_num);
/*关闭线程操作的套接字*/
close(new_fd[thrd_num]);
/*执行V操作*/
sem_post(&sem[thrd_num]);
/*将旗标设置为0,以便下一个线程使用*/
flag[thrd_num] = 0;
pthread_exit(NULL);
}
}
}
int main(void)
{
int i,no = 0;
int ret;
/*初始化缓冲区和信号量*/
for(i=0;i<MAXTHREAD;i++)
{
memset(buf[i],0,MAXDATASIZE);
sem_init(&sem[i],0,1);
}
/*创建套接字,类型为流式,默认TCP*/
if( (sock_fd=socket(AF_INET,SOCK_STREAM,0)) == -1 )
{
perror("socket");
exit(1);
}
/*设置协议族为IPv4,端口为自定义端口,地址为本地任意可用IP地址*/
server_ip.sin_family = AF_INET;
server_ip.sin_port = htons(MYPORT);
server_ip.sin_addr.s_addr = htonl(INADDR_ANY);
n = 1;
/*设置套接字选项,使其可以重复使用端口*/
setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));
/*绑定地址和端口信息*/
if( bind(sock_fd,(struct sockaddr *)&server_ip,sizeof(struct sockaddr)) == -1 )
{
perror("bind");
exit(1);
}
/*监听来自客户端的链接,队列长度为BACKLOG*/
if( listen(sock_fd,BACKLOG) == -1 )
{
perror("listen");
exit(1);
}
/*主操作部分*/
while(1)
{
sin_size = sizeof(struct sockaddr);
/*利用find函数找到合适的缓冲区,如果全部被占用,则从头依次向后排列,均衡阻塞*/
if( (ret=find()) == -1 )
{
no++;
no = no % MAXTHREAD;
/*等待占用线程结束,如果有多个在等待则形成等待队列*/
pthread_join(thread[no],NULL);
}
else
no = ret;
/*接受下一个客户端链接*/
if( (new_fd[no] = accept(sock_fd,(struct sockaddr *)&client_ip,&sin_size)) == -1 )
{
perror("accept");
exit(1);
}
/*创建线程*/
if( pthread_create(&thread[no],NULL,func,(void *)no) )
{
perror("pthread_create");
continue;
}
/*创建成功,设置旗标*/
else
{
printf("Thread %d has created!\n",no);
flag[no] = 1;
}
}
/*关闭服务器套接字*/
close(sock_fd);
return 0;
}
两个服务器端的效果一样,只是实现的方式不同。本人初学网络编程,文中必有瑕疵,如有高见,请赐教!!!!
本文探讨了并发服务器的实现,包括循环服务器与并发服务器的区别,并详细阐述了并发服务器通过创建子进程和使用线程的两种方法。重点讨论了使用子进程的方式,分析了父子进程间的资源独立性,以及在线程实现中如何处理共享资源的同步和调度,以实现高效且公平的缓冲区使用策略。
2184

被折叠的 条评论
为什么被折叠?



