最近跟老师做一个项目,里面用到了客户端服务器模型。本科的时候也做过,但是那时候纯粹是为了完成任务,没有考虑什么东西,只是实现了单个客户和服务器的通信,发送一些图片什么的。看到老师写的代码很牛逼,直接拿以前的代码的动态库过来就可以使用了,而且工作的很好,因此有了模仿的想法。
我的目的:构造一个灵活的服务器,顺便构造处理相同类型的客户端;服务器端:windows采用select 模型 和完成端口实现两个;linux采用epoll模型实现。客户端比较简单,谈不到什么模型的概念。
2012-7-28 实现了windows端的select模型的服务器:
select模型是一个比较简单的模型,但是在构造服务器方面其有一个短处,就是接待客户端的个数,这个个数受FD_SETSIZE的限制,windows上是64, linux上是1024。作为一个服务器,一般不能假设你的client少于某个数目,所以进行了改造下,运用线程来对客户进行分组,这样就没有了客户数量的限制了。
核心代码如下:
#include <stdio.h>
#include "server.h"
#include "wrap.h"
#define PORT 5150
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib")
DWORD WINAPI ProcessThread(LPVOID lpParam);
DWORD WINAPI ListenThread(LPVOID lpParam);
void NewClientIn(SOCKET client);
PROCESS_HANDLER req_handler;//回调函数,处理client请求
/*
虽然是个简单的select模型,但是还是要搞熟
问题:超过FD_SETSIZE个客户端的限制,一种方法是在#include <winsock.h>之上加上一个宏的定义,重新定义FD_SIZE为一个更大的数字但是这样做不太保险,可能引起系统的故障,因为可能和数据长度有关
第二种方法是每个FD_SETSIZE个客户分配一个线程,但是用户的加入倒还好,但是用户推出就比较难以处理,需要有一个线程的管理者来管理这些创建的线程。难点无法避免,同时也能让自己学习下线程的用法,编程学习之。
思路:
1.创建一个管理函数,用来管理新来的用户给哪个线程处理;若线程已满,则创建一个新的线程
2.数据结构:client采用一个节点;处理线程一个数据结构,存放一个链表和当前的套接字的数目;管理者一个链表,链接存储所有的线程数据结构
ok
*/
/*
第二步尝试:将用户请求的数据显示出来,然后回送回去 ok
*/
/*
第三步尝试:将该程序做成一个模块,其他模块直接调用即可
思路:首先这个程序的main函数需要改造,做成一个线程。函数自己的main函数在最后等待这个监听线程结束;
其次,扩展的数据结构的处理需要提供一个接口,这个接口的参数有请求的类型、请求的套接字和请求的内容,作为一个线程共享的全局回调函数来调用;
ok
现在只需要改动自己写好处理函数然后调用InitServer即可使用,处理函数处理用户需要的信息,具体的包在wrap中定义
*/
/*
函数调用结果均没有判断,加上去;以及临界区的管理(这个模块中链表是共享的数据结构,必须互斥访问)
*/
/*记录每个客户的socket信息*/
typedef struct{
SOCKET client; /*clien socket*/
LIST_HEAD sibling_list;/*client socket所在线程的下一个兄弟socket指针*/
}ClientSocket;
typedef struct{
LIST_HEAD head;/*the list head of clients of this thread*/
UINT16 sock_num;/*the client num of this thread*/
LIST_HEAD sibling_list;/*thread sibling pointer*/
}ThreadSocket;
LIST_HEAD thread_head;/*global variable for managing thread*/
CRITICAL_SECTION cri_sec;/*critical section variable*/
/*
辅助函数: 读取指定字节数的数据
*/
int recvNumBytes(SOCKET s, char *buf, int len)
{
int left = len;
int ret;
while( left > 0){
ret= recv(s, buf+len-left, left, 0);
left -=ret;
if(ret == 0 || (SOCKET_ERROR == ret && WSAECONNRESET == WSAGetLastError()))
return -1;
}
return len;
}
/*
此函数负责client的接入工作
*/
void NewClientIn(SOCKET client)
{
DWORD thread_id;
ClientSocket *new_client;
GBOOL if_new_thread = TRUE;
LIST_HEAD *list;
ThreadSocket *max_left_thread, *temp_thread;
UINT16 min_connected = FD_SETSIZE;
list_for_each(list, &thread_head)
{
temp_thread = ENTRY(list, ThreadSocket, sibling_list);
if(temp_thread->sock_num < min_connected)
{
min_connected = temp_thread->sock_num;
max_left_thread = temp_thread;
if_new_thread = FALSE;
}
}
if(if_new_thread)/*we need to create a new thread to process the incoming client*/
{
ThreadSocket *new_thread_socket;
new_thread_socket = (ThreadSocket *)malloc(sizeof(ThreadSocket));
new_client = (ClientSocket *)malloc(sizeof(ClientSocket));
if(NULL ==new_thread_socket || NULL == new_client)
{
EPRINTF("ERROR: sorry, I have encountered a memory error.\n");
exit(-1);
}
/*每个线程的第一个客户是不需要对数据进行互斥管理的,因为根本不会有线程访问这个线程数据*/
/*fill in client*/
new_client->client = client;
INIT_LIST_NODE(&(new_client->sibling_list));
/*fill in thread socket*/
INIT_LIST_HEAD(&(new_thread_socket->head));
INIT_LIST_NODE(&(new_thread_socket->sibling_list));
list_add_tail(&(new_client->sibling_list), &(new_thread_socket->head));
new_thread_socket->sock_num = 1;
/*fill in global thread info*/
list_add_tail(&(new_thread_socket->sibling_list), &(thread_head));
/*create thread*/
if(CreateThread(NULL, 0, ProcessThread, new_thread_socket, 0, &thread_id) == NULL)
{
EPRINTF("ERROR: sorry, I have encountered a thread error.\n");
exit(-1);/*exit the system*/
}
}
else
{
new_client = (ClientSocket *)malloc(sizeof(ClientSocket));
if(NULL == new_client)
{
EPRINTF("ERROR: sorry, I have encountered a memory error.\n");
exit(-1);
}
/*enter critical section*/
EnterCriticalSection(&cri_sec);
/*fill in client*/
new_client->client = client;
INIT_LIST_NODE(&(new_client->sibling_list));
/*add to thread info*/
list_add_tail(&(new_client->sibling_list),&(max_left_thread->head));
++max_left_thread->sock_num;
/*leave critical section*/
LeaveCriticalSection(&cri_sec);
/*printf the thread info*/
printf("Thread %p : client num %d\n", max_left_thread, max_left_thread->sock_num);
}
}
/*
处理用户请求的线程,每个线程最多处理FD_SETSIZE个客户
*/
DWORD WINAPI ProcessThread(LPVOID lpParam)
{
ThreadSocket *thread_socket= (ThreadSocket *)lpParam;
ClientSocket *client_socket;
fd_set fdread;
char szMessage[MSGSIZE];
LIST_HEAD *list;
struct timeval tv = {1, 0};
int ret;
Req_head head;
while (TRUE)
{
/*clear the set*/
FD_ZERO(&fdread);
/*add in all fd*/
list_for_each(list, &(thread_socket->head))
{
client_socket = ENTRY(list, ClientSocket, sibling_list);
FD_SET(client_socket->client, &fdread);
}
/*We only care read event*/
tv.tv_sec = 1;
ret = select(thread_socket->sock_num, &fdread, NULL, NULL, &tv);
/*no read event occur*/
if (ret == 0)
{
//Sleep(1000);/*in winxp, need to uncomment this line;win7 no need*/
continue;
}
/*check which socket has read event*/
list_for_each(list, &(thread_socket->head))
{
client_socket = ENTRY(list, ClientSocket, sibling_list);
if(FD_ISSET(client_socket->client,&fdread))
{
/*a read event has occured*/
ret = recvNumBytes(client_socket->client, &head, sizeof(Req_head));
/*error occur while reading*/
if(-1 == ret)
{
printf("client socket %d closed\n", client_socket->client);
list_del(&(client_socket->sibling_list));
--thread_socket->sock_num;
if(0 == thread_socket->sock_num)
{
list_del(&(thread_socket->sibling_list));
return 0;/*exit this thread*/
}
}
else{
/*read extra data*/
ret = recvNumBytes(client_socket->client, szMessage, head.length - sizeof(Req_head));
if(-1 == ret)
{
list_del(&(client_socket->sibling_list));
--thread_socket->sock_num;
if(0 == thread_socket->sock_num)
{
list_del(&(thread_socket->sibling_list));
return 0;/*exit this thread*/
}
}
/*process the req*/
req_handler(client_socket->client, head.type, szMessage, ret);
}
}
}//list for each
}
return 0;
}
DWORD WINAPI ListenThread(LPVOID lpParam)
{
SOCKET socket_listen, socket_client;
SOCKADDR_IN local, client;
int iaddrSize = sizeof(SOCKADDR_IN);
int count=0;
/*Create listening socket*/
if((socket_listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
{
EPRINTF("ERROR: Listen socket create error.\n");
exit(-1);
}
/*Bind*/
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
if(bind(socket_listen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
EPRINTF("ERROR: Bind socket to address error.\n");
exit(-1);
}
// Listen
if(listen(socket_listen, SOMAXCONN) == SOCKET_ERROR )
{
EPRINTF("ERROR: Listen client error.\n");
exit(-1);
}
while (TRUE)
{
printf("waiting for connect ...\n");
// Accept a connection
if((socket_client = accept(socket_listen, (struct sockaddr *)&client, &iaddrSize)) == INVALID_SOCKET )
{
EPRINTF("ERROR: Accept a client error.\n");
continue;/*abondon the client*/
}
NewClientIn(socket_client);
printf("Accepted %d client, This clien info:addr %s, port%d\n", ++count, inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
}
GBOOL *InitServer(PROCESS_HANDLER handler)
{
WSADATA wsa_data;
DWORD thread_id;
/*request handler initialize*/
if(NULL == handler)
return FALSE;
req_handler = handler;
/*init thread managerment data*/
INIT_LIST_HEAD(&thread_head);
/*init critical section variable*/
InitializeCriticalSection(&cri_sec);
/*Initialize Windows socket library*/
WSAStartup(0x0202, &wsa_data);
/*create listen thread*/
if(CreateThread(NULL, 0, ListenThread, NULL, 0, &thread_id) == NULL)
{
EPRINTF("ERROR: sorry, I have encountered a thread error.\n");
return FALSE;
}
return TRUE;
}
使用服务器很简单,先InitServer, 需要提供一个回调函数来对收到的请求进行处理。然后主函数需要一个机制来保证服务器线程退出之前,main函数不会退出。
既然是通信,肯定要设定一个协议,设定客户和服务器的数据的组织方式。我所采用的方式很简单,每个数据包分成两个部分,一个头head和一个数据块data,head中存放的是请求或者回复的类型和这个数据包的长度,data是请求的具体数据。
下面说下服务器的构造思路:
1. 首先,创建一个监听线程来接收用户的connect请求;
2. 当有client连接过来时,让一个管理函数来负责这个用户的“接待”工作:若没有负责数据通信的线程则创建一个线程来处理;若有多个线程来处理,选择一个负载最小的线程来处理;若客户退出来导致其负责通信的线程没有客户需要处理,则该线程自动退出。线程内部的管理相当简单,就是采用select模型对其负责的每个client进行询问,有请求则接收数据,然后调用InitServer指定的回调函数来处理该请求。
实现这个模型最主要的地方还是在于线程的管理,为此上面的文件中,设立了三个数据结构,采用链表对其进行了处理。
额,这个服务器的代码都在这里了,希望对其他人有参考作用。不懂可以email, rsqmail@163.com。
我来接着实现完成端口的服务器。