问题聚焦:
    在前面我们大概浏览了一下服务器编程需要掌握的一些知识和技术,以及架构思想。   
    实践,才是检验真理的唯一标准。。从这节起我们将在这些技术的基础上,一步步实现以及完善一个服务器,同时也是对这些技术的更深入的思考。
    本节我们将实现一个简单的回射服务器,包括TCP连接,文本处理,并发(多进程实现),以及子进程退出后的处理动作。

 
功能描述:
    客户端与服务器端进行TCP连接
    客户端从标准输入(键盘)读入一行文本,发送给服务器
    服务器从网络输入读取该行文本,并回射给客户
    客户从网络输入读入这行文本,并显示在标准输出(终端显示器)上
    支持并发(多进程实现)
 
封装:有,封装中处理了异常,报异常后退出程序。
 
架构:
 
语言:
    C++(因为考虑到后面重构会用类来封装,所以这里选择C++来实现,在本节基本没有体现出来。)
 
编译环境:
    Ubuntu12.04  g++
 
 
 
服务端代码:只贴出来关键代码,API翻翻书都看得懂,主要供自己大家参考这个流程
#include "mtserver.h"

int main(int argc, char* argv[])
{
checkArgc(argc, 2); const char* ip = argv[1];
int port = atoi( argv[2] ); /* 1 declare socket*/
int listenfd, connfd;
int ret; /* 2 initialize listen socket*/
mySocket(listenfd); /* 3 server address */
struct sockaddr_in servaddr;
initSockAddr(servaddr, ip, port); /* 4 bind */
myBind(listenfd,
(struct sockaddr*)&servaddr,
sizeof(servaddr)); /* 5 listen */
myListen(listenfd, 5); /* handle SIGCHLD signal*/
signal(SIGCHLD, handle_sigchild); /* 6 waiting for connecting */
pid_t chipid;
socklen_t clilen;
struct sockaddr_in cliaddr; for(;;) {
clilen = sizeof(cliaddr);
std::cout << "Waiting for connecting ..." << std::endl;
connfd = myAccept(listenfd,
(struct sockaddr*)&cliaddr,
&clilen);
printf("Connection %d established...\n", listenfd);
if ( (chipid=fork()) == 0 ) {
handle_recv(connfd);
}
} }

服务器端消息处理函数:
void handle_recv(int connfd) {

    char recvbuf[BUFSIZE];

    while(1) {
memset( recvbuf, '\0', BUFSIZE );
if ( recv(connfd, recvbuf,BUFSIZE,0) != 0) {
if (!strcmp(recvbuf, "exit"))
break;
fprintf(stderr,"recv msg: %s\n", recvbuf);
send(connfd, recvbuf, strlen(recvbuf), 0);
fprintf(stderr,"send back: %s\n\n", recvbuf);
}
}
close(connfd);
exit(0);
} void handle_sigchild(int signo)
{
pid_t pid;
int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 ) {
fprintf(stderr,
"child %d terminated\n",
pid);
return;
}
}

客户端代码:
#include "mtclient.h"

int main(int argc, char* argv[])
{
if (argc <=2 ) {
std::cout << "server ip and port needed." << std::endl;
return 1;
} int port = atoi(argv[2]);
char* ip = argv[1]; int sockfd;
struct sockaddr_in servaddr; mySocket(sockfd); initSockAddr(servaddr,ip, port); myConnect(sockfd,
(struct sockaddr*)&servaddr,
sizeof(servaddr)); handle_msg(sockfd);
exit(0); }

客户端消息处理函数:
void handle_msg(int sockfd) {
char sendbuf[BUFSIZE];
char recvbuf[BUFSIZE];
while(1) {
memset( sendbuf, '\0', BUFSIZE );
memset( recvbuf, '\0', BUFSIZE ); printf("%s", "send msg:");
gets(sendbuf);
if (strlen(sendbuf) > 0)
send(sockfd,sendbuf,strlen(sendbuf),0);
if ( !strcmp(sendbuf, "exit"))
break;
recv(sockfd,recvbuf,BUFSIZE,0);
printf("recv back:%s\n\n", recvbuf);
}
close( sockfd );
return;
}

代码里处理了僵尸进程的问题,下面重点看一下:
 
处理SIGCHLD信号
信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。
僵尸进程:占用内核空间,最终可能导致耗尽进程资源
所以,无论何时我们fork子进程都要wait它们。
当子进程结束后,会向父进程返回一个SIGCHLD信号,我们要捕获这个信号,并及时处理,防止出现僵尸进程
捕获的方法是signal系统调用,设置这个捕获的时机为在listen系统调用之后,fork子进程之前,且只做一次
 
signal系统调用
#include <signal.h>
_sighandler_t signal ( int sig, _sighandler_t _handler );

参数说明:
sig:要捕获的信号类型
_handler:指定信号sig的处理函数
 
慢系统调用:如accept,指那些可能永远阻塞的系统调用
适用于慢系统调用的规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。有些内核自动重启某些被中断的系统调用。
如果内核不自动重启这些系统调用,那么需要我们手动检查返回错误并处理
for( ; ; ) {
chilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (struct sockaddr* ) &cliaddr, &clilen )) < 0 )
{
if (errno == EINTR) // 检测到错误类型为EINTR时,重启accept系统调用
continue;
else
err_sys ("accept error");
}
}

我的环境是Ubuntu12.04,这里的accept是自动重启的,不过还是对EINTR信号进行了判断。
 
wait和waitpid函数
#include <sys/wait.h>
pid_t wait( int *statloc );
pid_t waitpid(pid_t pid, int *statloc, int options);

返回:已终止子进程的pid,以及通过statloc指针返回的子进程终止状态(一个整数)。
区别:
调用wait的进程没有已终止的子进程,不过有一个或多个子进程仍在执行,那么wait将阻塞到现有子进程第一个终止为止。
waitpid函数可以指定等待哪个进程,options参数允许我们指定附加选项,最常用的选项是WNOHANG,它告知内核在没有已终止子进程时不要阻塞。
使用:
void sig_child(int signo)
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0 )
printf("Child %d terminated\n", pid);
return;
}

运行截图:
 
客户端:

 
服务器端:

 
小结:我们在网络编程时可能会遇到三种情况
当fork子进程时,必须捕获SIGCHILD信号
当捕获信号时,必须处理被中断的系统调用
SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以避免留下僵死进程
 

 
TCP程序例子小结
客户端角度
 

 
服务器角度:

 
 
I/O复用的需求:
当服务器进程终止,客户进程没被告知。因为虽然客户的TCP确实被告知了,但是客户进程正阻塞于等待用户输入而为接收到该通知。
因此IO复用技术的必要性体现出来了。当然,我们之前就了解了IO复用技术,包括select,poll和epoll。
下一小节我们重点看一下IO复用的实现。
 

 
参考资料:
《Linux高性能服务器编程》
《UNIX网络编程 卷1:套接字联网API(第3版)》

服务器编程入门(10)TCP回射服务器实现 - 并发的更多相关文章

  1. TCP回射服务器修订版(ubuntu 18.04)

    一.需求 把https://www.cnblogs.com/soldierback/p/10673345.html中的TCP回射服务器程序重写成使用select来处理任意个客户的单进程 程序,而不是为 ...

  2. TCP回射服务器程序:main函数

    TCP回射并发服务器 1.创建套接字,绑定服务器的众所周知端口 创建一个TCP套接字,在待绑定到该TCP套接字的网际网套接字地址结构中填入通配地址(INADDR_ANY) 和服务器的众所知周(SERV ...

  3. UNIX网络编程——TCP回射服务器/客户端程序

    下面通过最简单的客户端/服务器程序的实例来学习socket API. serv.c 程序的功能是从客户端读取字符然后直接回射回去: #include<stdio.h> #include&l ...

  4. TCP回射服务器程序:str_echo函数

    str_echo函数执行处理每个客户的服务: 从客户读入数据,并把它们回射给客户 读入缓冲区并回射其中内容: read函数从套接字读入数据,writen函数把其中内容回射给客户 如果客户关闭连接,那么 ...

  5. 服务器编程入门(5)Linux服务器程序规范

    问题聚焦:     除了网络通信外,服务器程序通常还必须考虑许多其他细节问题,这些细节问题涉及面逛且零碎,而且基本上是模板式的,所以称之为服务器程序规范.     工欲善其事,必先利其器,这篇主要来探 ...

  6. UNIX网络编程——使用线程的TCP回射服务器程序

    同一进程内的所有线程除了共享全局变量外还共享: (1)进程指令: (2)大多数数据: (3)  打开的文件(即描述符): (4)信号处理函数和信号处置: (5)当前工作目录: (6)用户ID和组ID. ...

  7. 【Unix网络编程】chapter5TCP回射服务器程序

    chapter5  5.1 概述 5.2 TCP回射服务器程序:main函数 int main(int argc, char **argv) { int listenfd,connfd; pid_t ...

  8. TCP客户/服务器程序实例——回射服务器

    目录 客户/服务器程序源码 POSIX信号处理 POSIX信号语义 处理SIGCHLD信号 处理僵死进程 处理被中断的系统调用 wait和waitpid函数 wait和waitpid函数的区别 网络编 ...

  9. 【Unix网络编程】 chapter5 TCP客户,服务器程序实例

    chapter5 5.1 概述 5.2 TCP回射服务器程序:main函数 int main(int argc, char **argv) { int listenfd,connfd; pid_t c ...

随机推荐

  1. Swift - 使用相机拍摄照片

    1,打开相机拍照 通过设置图片控制器UIImagePickerController的来源为UIImagePickerControllerSourceType.Camera,便可以打开相机 1 2 3 ...

  2. 让Delphi XE2程序支持UAC

    在win7下,开发的程序有的时候莫名其妙就不能正常工作了,其实都是因为权限不够,要想能够正常运行,就需要获得管理员权限,这就需要处理UAC.具体方法如下: 一,制作“uac.manifest”文件.新 ...

  3. 暴力拆解CPU

    http://www.ruanyifeng.com/blog/2010/11/cpu_autopsy.htmlhttp://bbs.mydigit.cn/read.php?tid=110272http ...

  4. js / ajax 成功提交后怎么跳转到另外一个页面?

    把success那段改成 success : function (r) { if ( r.status == 'error' ){ alert(msg[r.msgno]); } else if (r. ...

  5. Maven插件之portable-config-maven-plugin(不同环境打包)

    在大型的项目组中,分不同的开发环境,测试环境,生产环境(说白了就是配置文件不同,或者数据源,或者服务器,或者数据库等);问题来了,如何使用Maven针对不同的环境来打包呢? Maven提供了Profi ...

  6. 菜鸟版JAVA设计模式—从买房子看代理模式

    今天学习了代理模式. 相对于适配器模式,或者说装饰器模式,代理模式理解起来更加简单. 代理这个词应该比較好理解,取代去做就是代理. 比方,我们买卖房子,那么我们会找中介,我要卖房子,可是我们没有时间去 ...

  7. HDOJ 2442 -bricks 状态压缩DP 一直TLE.打表过的..

    有5个砖块..加上一个空着不放..那么有6种状态..所以很明显的可以用6进制的状态DP... 不过这么做..我觉得我已经能优化的都优化了...还是超时..一看数据范围是100*6..打表先AC了.. ...

  8. android 请求网络 和 httpclient的使用上传下载

    访问网络最主要的也就是 http协议了. http协议很简单,但是很重要. 直接上代码了,里面都是1个代码块 代码块的,用哪一部分直接拷出去用就好了. 1.访问网络用 get 和 post  自己组拼 ...

  9. 终于懂了:Delphi消息的Result域出现的原因——要代替回调函数的返回值!(MakeObjectInstance不会帮助处理(接收)消息回调函数的返回值)

    MakeObjectInstance应该不会帮助处理(接收)消息回调函数的返回值,可是有时候又确实需要这个返回值,这可怎么办呢?我是看到这段文字的时候,想到这个问题的: 当WM_PAINT不是由Inv ...

  10. Android 服务类Service 的具体学习

    上一篇说到了通知栏Notification,提起通知栏,不得让人想到Service以及BroadcastReceive,作为android的4大组建的2个重要成员,我们没少和它们打交道.它们能够在无形 ...