UNP学习笔记(第五章 TCP客户/服务程序实例)
我们将在本章使用前一章中介绍的基本函数编写一个完整的TCP客户/服务器程序实例
这个简单得例子是执行如下步骤的一个回射服务器:

TCP回射服务器程序
#include "unp.h" int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, ); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); for ( ; ; ) {
clilen = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &clilen); if ( (childpid = Fork()) == ) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit();
}
Close(connfd); /* parent closes connected socket */
}
}
str_echo函数
#include "unp.h" void
str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE]; again:
while ( (n = read(sockfd, buf, MAXLINE)) > )
Writen(sockfd, buf, n); if (n < && errno == EINTR)
goto again;
else if (n < )
err_sys("str_echo: read error");
}
TCP回射客户程序
#include "unp.h" int
main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr; if (argc != )
err_quit("usage: tcpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_STREAM, ); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[], &servaddr.sin_addr); Connect(sockfd, (SA *) &servaddr, sizeof(servaddr)); str_cli(stdin, sockfd); /* do it all */ exit();
}
str_cli函数
#include "unp.h" void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == )
err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout);
}
}
正常启动
在后台启动服务器

服务器启动后,它将阻塞于accept调用。运行netstat程序来检查服务器监听套接字状态

端口9877是我们服务器使用的端口,netstat用星号“*”表示一个为0的IP地址(INADDR_ANY 通配地址)或为0的端口号
使用环回地址启动客户端

客户端程序调用socket和connest,将阻塞带fgets调用。
服务器中的accept返回,然后调用fork,子进程调用str_echo,阻塞于read函数。父进程再次调用accept并阻塞。
此时,我们有3个都在睡眠(即已阻塞):客户进程、服务器父进程和服务器子进程
使用nestat给出对应所建立TCP连接。


第一个是服务器父进程,第二个是客户进程,第三个是服务器子进程。
正常终止程序

我们输入两行数据,每行都得到回射。我们接着键入终端EOF字符Ctrl+D以终止客户(导致fgets返回一个空指针)
POSIX信号处理
关于信号我们可以查看以前写的apue学习笔记 http://www.cnblogs.com/runnyu/p/4641346.html
关于进程可以查看 http://www.cnblogs.com/runnyu/p/4638913.html
这是后面章节的基本知识(例如signal、wait函数)
处理SIGCHLD信号
设置僵死状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。
处理僵死进程
在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程。按系统默认将忽略此信号。
我们可以在listen调用之后捕获SIGCHLD信号用来处理僵死进程
Signal(SIGCHLD,sig_chld); #include "unp.h" void
sig_chld(int signo)
{
pid_t pid;
int stat; pid = wait(&stat);
printf("child %d terminated\n", pid);
return;
}
处理被中断的系统调用
当阻塞与某个慢系统调用(如accept,read)的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回EINTR错误。
有些内核自动重启某些被中断的系统调用。不过为了便于移植,我们在编写程序时必须对慢系统调用返回EINTR有所准备。
例如,为了处理被中断的accept。我们把上面accept的调用改成如下所示
for ( ; ; ) {
clilen = sizeof(cliaddr);
if((connfd=accept(listenfd,(SA *)&cliaddr,&clilen))<){
if(errno==EINTR)
continue;/* back to for() */
else
err_sys("accept error");
}
这段代码所做的事情就是自己重启被中断的系统调用
wait和waipid
建立一个信号处理函数并在其中调用wait并不足以防止出现僵死进程。
考虑5个客户端同时结束,服务器子进程同时发送5次SIGCHLD信号,因为UNIX信号一般是不排队的,因此信号处理函数可能只执行一次,而留下4个僵死进程。
正确的解决办法是调用waitpid而不是wait,下面给出正确处理SIGCHLD的sig_chld函数。
#include "unp.h" void
sig_chld(int signo)
{
pid_t pid;
int stat; while ( (pid = waitpid(-, &stat, WNOHANG)) > )
printf("child %d terminated\n", pid);
return;
}
我们在一个循环内调用waitpid(-1代表等待任意子进程),以获取所有已终止子进程的状态。
我们必须指定WNOHANG选项,它告知waitpid在尚未终止的子进程在运行时不要阻塞。
下面给出我们的服务器程序的最终版本
#include "unp.h" int
main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int); listenfd = Socket(AF_INET, SOCK_STREAM, ); bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT); Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); Signal(SIGCHLD, sig_chld); /* must call waitpid() */ for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < ) {
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
} if ( (childpid = Fork()) == ) { /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit();
}
Close(connfd); /* parent closes connected socket */
}
}
服务器进程终止
1.我们在同一个主机上启动服务器和客户端,并在客户上键入一行文本,以验证一切正常。

2.找到服务器子进程的进程ID,并执行kill命令杀死它。这导致向客户发送一个FIN,而客户则相应以一个ACK。

3.SIGCHLD信号被发送给服务器父进程,并得到正确处理
4.客户端接收来自服务器TCP的FIN并相应一个ACK,然后问题是客户进程阻塞在fgets调用上,等待从终端接收一行文本。
5.键入netstat命令,以观察套接字状态

可以看到,TCP连接中止序列的前半部分已经完成
6.在客户上再键入一行文本

str_cli调用writen,客户TCP接着把数据发送给服务器。
TCP允许这么做,因为客户TCP接收到FIN只是表示服务器进程已关闭了连接的服务器端,从而不再往其中发送任何数据而已。FIN的接收并没有告知客户TCP服务器进程已经终止。
当服务器TCP接收到来自客户的数据时,既然先前打开那个套接字的进程已经终止,于是相应以一个RST
然而客户进程看不到这个RST,因为它在调用writen后立即调用readlind,并且由于第二步接收的FIN,所调用的readline立即返回0。
于是以出错信息“server terminated prematurely”退出,客户端终止,关闭所有打开的描述符。
当FIN到达套接字时,客户正阻塞在fgets调用上。客户实际上在应对两个描述符--套接字和用户输入。
事实上程序不应该阻塞到两个源中某个特定源的输入上,而是应该阻塞在其中任何一个源的输入上,这正是select和poll这两个函数的目的之一。
下一章我们将重新编写str_cli函数:一旦杀死服务器子进程,客户就会立即被告知已收到的FIN。
SIGPIPE信号
当一个进程向某个已收到RST的套接字执行写操作(返回EPIPE错误)时,内核向该进程发送一个SIGPIPE信号。该信号的默认行为是终止进程,因此进程必须捕获它以免不情愿地被终止。
服务器主机崩溃
使用下面步骤来模拟服务器崩溃:
在不同主机上运行客户和服务器。先启动服务器,再启动客户(键入一行文本以确定连接工作正常),然后从网络上断开服务器主机,在客户上键入另一行文本。
1.当服务器主机崩溃时,已有的网络连接上不发出任何东西。
2.我们在客户上键入一行文本,它由writen写入内核,再由客户TCP作为一个数据分节送出。客户随后阻塞于readline调用。
3.客户TCP持续重传数据分节,试图从服务器上接收一个ACK。如果在放弃重传前服务器主机没有重新启动,则客户进程返回一个错误。
既然客户阻塞在readline调用上,该调用将返回一个错误。假设服务器主机已崩溃,从而客户的数据分节根本没有相应,那么所返回的错误是ETIMEOUT。
然后如果某个中间路由器判断服务器主机已不可达,从而相应一个“destination unreachable”ICMP消息,那么所返回的错误是EHOSTUNREACH或ENETUNREACH。
服务器主机崩溃后重启
当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节相应一个RST
当客户TCP收到该RST时,客户正阻塞与readline调用,导致调用返回ECONNERESET错误。
服务器主机关机
UNIX系统关机时,init进程通常先给所有进程发送SIGTERM信号(可以被捕获),等待一段固定的时间,然后给所有还在运行的进程发送SIGKILL信号(不能捕获)。
当服务器进程接收到信号终止时,它所打开的描述符都被关闭,随后发生与上面服务器进程终止所讨论的一样。
例子:在客户与服务器之间传递文本串
对两个数求和的str_echo函数
#include "unp.h" void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == )
err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout);
}
}
例子:在客户与服务器之间传递二进制结构
头文件sum.h
struct args {
long arg1;
long arg2;
};
struct result {
long sum;
};
发送两个二进制整数给服务器的str_cli函数
#include "unp.h"
#include "sum.h" void
str_cli(FILE *fp, int sockfd)
{
char sendline[MAXLINE];
struct args args;
struct result result; while (Fgets(sendline, MAXLINE, fp) != NULL) { if (sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != ) {
printf("invalid input: %s", sendline);
continue;
}
Writen(sockfd, &args, sizeof(args)); if (Readn(sockfd, &result, sizeof(result)) == )
err_quit("str_cli: server terminated prematurely"); printf("%ld\n", result.sum);
}
}
对两个二进制整数求和的str_echo函数
#include "unp.h"
#include "sum.h" void
str_echo(int sockfd)
{
ssize_t n;
struct args args;
struct result result; for ( ; ; ) {
if ( (n = Readn(sockfd, &args, sizeof(args))) == )
return; /* connection closed by other end */ result.sum = args.arg1 + args.arg2;
Writen(sockfd, &result, sizeof(result));
}
}
UNP学习笔记(第五章 TCP客户/服务程序实例)的更多相关文章
- Programming Entity Framework-dbContext 学习笔记第五章
### Programming Entity Framework-dbContext 学习笔记 第五章 将图表添加到Context中的方式及容易出现的错误 方法 结果 警告 Add Root 图标中的 ...
- [HeadFrist-HTMLCSS学习笔记]第五章认识媒体:给网页添加图像
[HeadFrist-HTMLCSS学习笔记]第五章认识媒体:给网页添加图像 干货 JPEG.PNG.GIF有何不同 JPEG适合连续色调图像,如照片:不支持透明度:不支持动画:有损格式 PNG适合单 ...
- 第四章 基本TCP套接字编程 第五章 TCP客户/服务器程序实例
TCP客户与服务器进程之间发生的重大事件时间表 TCP服务器 socket() --- bind() --- listen() --- accept() --- read() --- write -- ...
- 【马克-to-win】学习笔记—— 第五章 异常Exception
第五章 异常Exception [学习笔记] [参考:JDK中文(类 Exception)] java.lang.Object java.lang.Throwable java.lang.Except ...
- UNP学习笔记(第二章:传输层)
本章的焦点是传输层,包括TCP.UDP和SCTP. 绝大多数客户/服务器网络应用使用TCP或UDP.SCTP是一个较新的协议. UDP是一个简单的.不可靠的数据报协议.而TCP是一个复杂.可靠的字节流 ...
- 《Spring实战》学习笔记-第五章:构建Spring web应用
之前一直在看<Spring实战>第三版,看到第五章时发现很多东西已经过时被废弃了,于是现在开始读<Spring实战>第四版了,章节安排与之前不同了,里面应用的应该是最新的技术. ...
- opencv图像处理基础 (《OpenCV编程入门--毛星云》学习笔记一---五章)
#include <QCoreApplication> #include <opencv2/core/core.hpp> #include <opencv2/highgu ...
- 学习笔记 第五章 使用CSS美化网页文本
第五章 使用CSS美化网页文本 学习重点 定义字体类型.大小.颜色等字体样式: 设计文本样式,如对齐.行高.间距等: 能够灵活设计美观.实用的网页正文版式. 5.1 字体样式 5.1.1 定义字体 ...
- [汇编学习笔记][第五章[BX]和loop指令]
第五章[BX]和loop指令 前言 定义描述性符号“()”来表示一个寄存器或一个内存单元的内容,比如: (ax)表示ax中的内容,(al)表示al的内容. 约定符号ideta表示常量. 5.1 [BX ...
随机推荐
- POJ 2723 Get Luffy Out(2-SAT+二分答案)
Get Luffy Out Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 8851 Accepted: 3441 Des ...
- vue经验总结
1. vue中获取dom节点时机 vue组件中获取dom节点一定要在mounted周期之后的下一次事件循环,包括 component.$refs,component.$el,component.$ch ...
- bzoj1093【ZJOI2007】最大半联通子图
题意:http://www.lydsy.com/JudgeOnline/problem.php?id=1093 sol :一开始理解错题意了QAQ,还莫名其妙写挂了QAQ,调了半天 首先显然一个强联 ...
- easyUI 接收Spring Mvc中@ResponseBody中文乱码解决
接触springMVC不够深入,乱码困扰我到深夜,特此留下记忆: @responsebody默认滴是ISO-8859-1 Controller注解参数 @ResponseBody 标注后返回Strin ...
- 获取浏览器的homepage
主要知识点:跨进程访问数据 首先修改浏览器源码:BrowserSettings.java private static String getSDMCDefaultSharedPreferencesNa ...
- callee返回正被执行的Function对象
arguments.length是实参长度, arguments.callee.length是形参长度. function fn(a, b, c, d) { console.log(arguments ...
- CF 148D Bag of mice【概率DP】
D. Bag of mice time limit per test 2 seconds memory limit per test 256 megabytes Promblem descriptio ...
- poj 3693 Maximum repetition substring 重复次数最多的连续子串
题目链接 题意 对于任意的字符串,定义它的 重复次数 为:它最多可被划分成的完全相同的子串个数.例如:ababab 的重复次数为3,ababa 的重复次数为1. 现给定一字符串,求它的一个子串,其重复 ...
- Day 26 python 正则表达式
re模块\正则表达式 一.元字符 1.. ^ $ * + ? { } [ ] | ( ) \ "." 代表(任意一个字符) "*" 代表(任意数量任意字符,0- ...
- bit、byte、位、字节、汉字的关系
字节(Byte):通常将可表示常用英文字符8位二进制称为一字节. 一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间. 符号:英文标点2占一个字节,中文标点占两个字节. 1字节( ...