网络编程套接字(7)——多线程版本的简单TCP网络程序

通过每个请求创建一个线程的方式来支持多连接:

server.c:

#include #include#include #include #include #include #include #include #include#define MAX 128typedef struct { int sock; char ip[24]; int port; }net_info_t; int Startup(char* ip,int port){ int sock = socket(AF_INET,SOCK_STREAM,0); if(sock < 0){ printf("socket error!\n"); exit(2); } struct sockaddr_in local; local.sin_family = AF_INET; local.sin_addr.s_addr = inet_addr(ip); local.sin_port = htons(port); if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0){ printf("bind error!\n"); exit(3); }if(listen(sock,5) < 0){ printf("listen error!\n"); exit(4); }return sock; }void service(int sock,char* ip,int port){ char buf[MAX]; while(1){ buf[0] = 0; ssize_t s = read(sock,buf,sizeof(buf)-1); if(s > 0){ buf[s] = 0; printf("[%s:%d] say# %s\n",ip,port,buf); write(sock,buf,strlen(buf)); } else if(s == 0){ printf("client [%s:%d] quit!\n",ip,port); break; } else{ printf("read error!\n"); break; } } }void* thread_service(void* arg){ net_info_t* p = (net_info_t*)arg; service(p->sock,p->ip,p->port); close(p->sock); free(p); }int main(int argc,char* argv[]){ if(argc != 3){ printf("Usage:%s [ip] [port]\n",argv[0]); return 1; } int listen_sock = Startup(argv[1],atoi(argv[2])); struct sockaddr_in peer; char ipBuf[24]; for(; ; ){ ipBuf[0] = 0; socklen_t len = sizeof(peer); int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len); if(new_sock < 0){ printf("accept error!\n"); continue; } inet_ntop(AF_INET,(const void*)&peer.sin_addr,ipBuf,sizeof(ipBuf)); int p = ntohs(peer.sin_port); printf("get a new connect,[%s:%d]\n",ipBuf,p); net_info_t *info = (net_info_t*)malloc(sizeof(net_info_t)); if(info == NULL){ perror("malloc"); close(new_sock); continue; } info->sock = new_sock; strcpy(info->ip,ipBuf); info->port = p; pthread_t tid; pthread_create(&tid,NULL,thread_service,(void*)info); pthread_detach(tid); } return 0; }

client.c:

#include #include #include #include #include #include #include #include#define MAX 128int main(int argc,char* argv[]){ if(argc != 3){ printf("Usage:%s [ip] [port]\n",argv[0]); return 1; } int sock = socket(AF_INET,SOCK_STREAM,0); if(sock < 0){ printf("socket error!\n"); return 2; }struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); server.sin_addr.s_addr = inet_addr(argv[1]); if(connect(sock,(struct sockaddr*)&server,sizeof(server)) < 0){ printf("connect error!\n"); return 3; }char buf[MAX]; while(1){ printf("please Enter# "); fflush(stdout); ssize_t s = read(0,buf,sizeof(buf)-1); if(s > 0){ buf[s-1] = 0; if(strcmp("quit",buf) == 0){ printf("client quit!\n"); break; } write(sock,buf,strlen(buf)); s = read(sock,buf,sizeof(buf)-1); buf[s] = 0; printf("server Echo# %s\n",buf); } } close(sock); return 0; }

  • 多线程版本的优缺点
优点:

【网络编程套接字(7)——多线程版本的简单TCP网络程序】(1)能处理多用户请求;
(2)代码较为简单,编写周期短。


缺点:
(1)连接到来时才创建子进程,而创建子进程需要浪费时间,会影响性能;
(2)多线程服务器的每个进程都占用资源,进而导致其能服务的客户数量有限;

(3)多线程服务器随着进程数量的增多,CPU的压力会变大,CPU调度所需的时间会变长,会影响性能。

(4)多线程服务器的稳定性差,可能因为线程安全问题使得整个服务器挂掉。


  • 进程池与线程池
1.池

由于服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就是池的概念。
池是一组资源的集合,这组资源在服务器启动之初就被创建并初始化,这称为静态资源分配。
当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。
当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用设施,它避免了服务器对内核的频繁访问。提高了效率。
池可以分为很多种,常见的有进程池,线程池,内存池。
2.进程池&&线程池

在面向对象程序编程中,对象的创建与析构都是一个较为复杂的过程,较费时间,所以为了提高程序的运行效率尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。
所以我们可以创建一个进程池(线程池),预先放一些进程(线程)进去,要用的时候就直接调用,用完之后再把进程(线程)归还给进程池(线程池),省下创建删除进程的时间,不过当然就需要额外的开销了。
利用线程池与进程池可以使管理进程与线程的工作交给系统管理,不需要程序员对里面的线程、进程进行管理。
进程池和线程池的好处是减少了创建、归还的时间,从而提高了效率。

进程池和线程池相似,所以这里我们以进程池为例进行介绍。若无特殊声明,下面对进程池的描述也适用于线程池。
进程池是由服务器预先创建的一组子进程,这些子进程的数目在 3~10 个之间(当然这只是典型情况)。线程池中的线程数量应该和 CPU 数量差不多。
进程池中的所有子进程都运行着相同的代码,并具有相同的属性,比如优先级、 PGID 等。
当有新的任务来到时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比于动态创建子进程,选择一个已经存在的子进程的代价显得小得多。至于主进程选择哪个子进程来为新任务服务,则有两种方法:
1)主进程使用某种算法来主动选择子进程。最简单、最常用的算法是随机算法和 Round Robin (轮流算法)。
2)主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。最简单的方式是,在父进程和子进程之间预先建立好一条管道,然后通过管道来实现所有的进程间通信。在父线程和子线程之间传递数据就要简单得多,因为我们可以把这些数据定义为全局,那么它们本身就是被所有线程共享的。
线程池主要用于:
1)需要大量的线程来完成任务,且完成任务的时间比较短。 比如WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大。但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2)对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3)接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。





    推荐阅读