《Unix 网络编程》05:TCP C/S 程序示例
TCP客户/服务器程序示例
系列文章导航:《Unix 网络编程》笔记
目标
ECHO-Application 结构如下:
A[标准输入/输出] --fgets--> B[TCP-Client] --writen/read--> C[TCP-Server]
C --readline/writen--> B --fputs--> A
除此之外,还有:
- Client 和 Server 启动时发生什么
- Client 正常终止时发生什么
- Server 先意外终止会发生什么
程序代码
服务端
#include "unp.h"
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
// 创建 Socket
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
// 初始化连接参数
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()) == 0)
{ /* child process */
Close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
exit(0);
}
Close(connfd); /* parent closes connected socket */
}
}
#include "unp.h"
void str_echo(int sockfd)
{
ssize_t n;
char buf[MAXLINE];
again:
while ((n = read(sockfd, buf, MAXLINE)) > 0)
Writen(sockfd, buf, n);
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
客户端
#include "unp.h"
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in servaddr;
if (argc != 2)
err_quit("usage: tcpcli <IPaddress>");
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd, (SA *) &servaddr, sizeof(servaddr));
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
#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) == 0)
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout);
}
}
正常情况
当我们把服务器和客户端都启动后,可以通过命令查看网络的情况:
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:9877 localhost:38160 ESTABLISHED
tcp 0 0 localhost:38160 localhost:9877 ESTABLISHED
- 第一个是服务器的父进程,状态为 LISTEN,监听范围和可接受范围如上所示
- 第二个是客户端进程
- 第三个是服务器的子进程,为客户端提供具体的服务
连接的正常断开
我们在客户端输入 EOF (Ctrl + D),之后会发生一系列事情:
autonumber
participant cs as cli_str
participant cm as cli_main
participant sm as serv_main_child
participant ss as serv_str
cs ->> cm: fgets获得EOF,函数返回
cm ->> cm: 执行完毕, 调用 exit 结束
cm ->> ss: 关闭 cli 打开的所有描述符,并发送 FIN 给客户
note over cm: FIN_WAIT_1
ss ->> sm: readline 接受到 FIN,返回0,函数返回
sm ->> sm: 执行完毕,调用 exit 结束子进程
note over sm: CLOSE_WAIT
sm ->> cm: 关闭所有打开的描述符,发送ACK
note over sm: LAST_ACK
note over cm: FIN_WAIT_2
sm ->> cm: FIN
note over cm: TIME_WAIT
cm ->> sm: ACK
note over sm: CLOSED
(上述如套接字的操作其实是在内核完成的,这里为了简便所以标在了对应的线程上)
如下,可以看到客户端的 TIME_WAIT 状态持续了一段时间
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
tcp 0 0 localhost:38160 localhost:9877 TIME_WAIT
[root@centos-5610 Unix_Network]# netstat -a | grep 9877
tcp 0 0 0.0.0.0:9877 0.0.0.0:* LISTEN
POSIX信号处理
僵死进程
背景
在上述程序中,其实子进程结束后,会向父进程发送一个 SIGCHLD
信号,我们这里没有捕捉,默认行为为被忽略。
既然父进程未加处理,子进程于是进入僵死状态,如下状态 Z 所示:
[root@centos-5610 Unix_Network]# ps -t pts/0 -o pid,ppid,stat,tty,args,wchan
PID PPID STAT TT COMMAND WCHAN
2008 1771 S pts/0 ./tcpserv01 inet_csk_accept
2382 2008 Z pts/0 [tcpserv01] <defunct> do_exit
或如下所示:
[root@centos-5610 tcpcliserv]# ps
PID TTY TIME CMD
1771 pts/0 00:00:00 bash
2008 pts/0 00:00:00 tcpserv01
2382 pts/0 00:00:00 tcpserv01 <defunct>
2555 pts/0 00:00:00 tcpserv01 <defunct>
2654 pts/0 00:00:00 tcpserv01 <defunct>
2886 pts/0 00:00:00 tcpserv01 <defunct>
3238 pts/0 00:00:00 tcpserv01 <defunct>
6685 pts/0 00:00:00 ps
为什么会有僵死进程
设置僵死的目的是维护子进程的信息,以便父进程在以后某个时候获取这些信息(包括进程 ID、终止状态、资源利用情况)
父进程终止了,还有人管这些僵死进程吗
如果父进程也终止了,而其有处于僵死状态的子进程,那么子进程的父进程会被设置为 1(init 进程的 ID),init 进程会清理他们(wait,后续讲解)
僵死进程的坏处
他们占用内核的空间,最终可能导致我们耗尽处理资源,所以我们必须处理僵死进程。
信号基础
信号就是告知某个进程发生了某个事件的通知,有时也称为软件中断。
信号的来源
- 一个进程发送给另一个进程(或自身)
- 由内核发给某个进程
信号的处理
通过调用 sigaction
函数设定一个信号的处理,并有三种选择:
- 设置一个信号处理函数。SIGKILL 和 SIGSTOP 不能被捕获
- 设定为
SIG_IGN
来忽略它。同样,上述两个信号不能被忽略 - 设定为
SIG_DFL
来启用他的默认处置。默认处置通常是终止进程
signal
sigaction 函数太过于复杂,所以一般我们会调用 signal 函数。
但是 signal 函数由于历史和标准的原因在不同的系统上实现不一致,所以我们实现自己的 signal 方法。其签名如下:
void (*signal(int signo, void (*func)(int)))(int);
我们会做一些处理,简化其表示:
typedef void Sigfunc(int);
Sigfunc *signal(int signo, Sigfunc * func);
signal 函数如下:
#include "unp.h"
Sigfunc *signal(int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM)
{
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
}
else
{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}
处理 SIGCHLD 信号
建立一个俘获 SIGCHLD 信号的信号处理函数,在函数体中调用 wait(后面会提到):
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;
}
在 Listen 方法后调用:(必须在 fork 前调用,且只能执行一次)
Listen(listenfd, LISTENQ);
Signal(SIGCHLD, sig_chld);
此时就不会再出现僵死进程了。
被中断的系统调用
慢系统调用
如 accept 等函数,如果没有用户连接,将一直阻塞下去,把这样的系统调用称为慢系统调用。
满系统调用的中断
如前一节我们处理 SIGCHLD 信号时,当系统阻塞于一个慢系统调用时,而该进程又捕获了一个信号,且相应的信号处理函数返回时,该系统调用可能会返回一个 IENTER 错误。
有些系统可能会自动重启某些被中断的系统调用,但是出于对程序的可移植性考虑,我们应该对此有所准备。
for (;;) {
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA *)&cliaddr, &clilen)) < 0) {
if (errno == EINTER) {
continue;
}
else {
err_sys("XXX");
}
}
}
这种方式对 accept
以及诸如 read、write、select、open 之类的函数来说都是合适的,但是如前面所说,connect 函数不能重启。
wait 和 waitpid
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
相同之处
均返回已终止子进程的 ID,以及通过 statloc 指针返回的子进程终止状态
一些宏 WIFEXIST、WEXITSTATUS 可以用来查看其信息
不同之处
如果调用 wait 的进程没有已终止的子进程,则阻塞至第一个现有子进程终止为止
而 waitpid 可以通过 pid 和 options 参数来进行更多的控制
wait 的问题
如果我们用多台客户端发送请求,然后同时终止,如下:
多个 SIGCHLD 信号会到达,但是 wait 只会被执行一次,导致会留下 4 个僵死进程,如果是在不同的机器上执行的,则更为不确定。
用 waitpid 可以解决这个问题:
#include "unp.h"
void sig_chld(int signo) {
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}
- WNOHANG 表示如果没有终止的子进程就阻塞(因此我们可以用 while 循环)
异常情况
accept 返回前连接中止
连接刚刚建立,客户端就发送一个 RST。
什么样的场景下会发生这种事情?我在网上简单检索了一下,但是没有找到典型的发生场景。
书上给出的例子是 Web 服务器比较繁忙
如何处理这种情况取决于具体的实现。
服务器进程终止
这里指的是服务器的子进程,也就是提供具体服务的那个进程。
我们先把 server/client 启动,然后把子进程关闭掉,观察现象:
[root@centos-5610 tcpcliserv]# ./tcpcli01 127.0.0.1
>>1
str_cli: server terminated prematurely
- 如果我们什么也不做,那么客户端会一直被 fgets 阻塞,它对外界发生的事情一无所知
- 如果我们发送什么新的信息,那么会出现一个报错信息
过程解释
- 服务器的相关 socket 关闭后,会发送一个 FIN 个客户端
- 客户端 socket 虽然接收到了,但是这只表示服务器进程关闭了连接的服务器端,从而不在往其中发送任何消息了,但并没有告知客户 TCP 服务器进程已经终止;所以客户端还是可以发送 writen 的
- 当服务器 TCP 接收到来自客户的数据时,由于该 TCP 已经被关闭,所以会相应一个 RST
- 客户端在调用 write 后便进入 readline,于是接收到了 TCP 之前发送到的 FIN (客户端没有接收到 RST),这将使 readline 返回 0,程序结束
- 客户端进行关闭资源的各项操作
本例的问题在于:
- 客户端同时应对了两个描述符:套接字和用户输入
- 客户端应该阻塞在其中任何一个源的输入上,而不是单纯地阻塞在这两个源中某个特定源的输入上
这正是后文 select 和 poll 这两个函数的目的之一;后文经过修改,即可让程序立刻对服务器的 FIN 进行处理
SIGPIPE 信号
- 前文:接收到客户端的 FIN 后,仍然可以 write 写数据
- 但是,如果接收到了服务器的 RST,此时如果再写数据,就会由内核向进程发送一个 SIGPIPE 信号;此信号的默认行为是终止进程
- 我们可以捕获该信号,不过无论是否捕获,readline 还是会返回一个 EPIPE 错误
服务器主机崩溃
例如服务器宕机了,这种情况下服务器来不及交代“遗言”就挂掉了。
发生的事情
- 如果客户端不发送消息,则会想上文提到的场景一样,永远等下去
- 如果发送消息,则会由于接收不到服务器的响应而不断尝试重新发送,书中等待了 9 分钟才放弃发送,返回
ETIMEDOUT
,如果被路由器判定不可达,则返回EHOSTUNREACH
或ENETUNREACH
改进
- 对于上述第一种问题,可以采用后文的
SO-KEEPALIVE
套接字选项 - 第二个人问题可以对
readline
设置一个超时
如果服务器重启
- 尽管重启了,但是 TCP 套接字的信息都丢失了
- 只能对发过来的请求说:我认识你吗(RST)
- 客户端 readline 接收到 RST 后,返回
ECONNRESET
错误
服务器主机关机
Unix 系统关机时,会“先礼后兵”:
- 先发送
SIGTERM
信号给所有进程,在一段时间后再发送SIGKILL
信号 - 接收到
SIGTERM
进程一般会进行一些善后操作,如果进程不捕获这个信号,那他的默认行为就是终止进程 SIGKILL
会让所有进程终止,自然也会释放套接字等信息
数据格式
由于如下的问题:
- 不同的实现以不同的方式存储二进制,如大小端字节序
- 不同的实现在存储相同的 C 数据类型上的差异
- 不同的实现给结构打包的方式存在差异
所以通过套接字传输二进制数据是不明智的。
解决方法有:
- 把所有的数值数据作为文本串来传递
- 显示定义所支持数据类型的二进制格式,并传输此格式的数据,如 RPC 通常包括这种技术
《Unix 网络编程》05:TCP C/S 程序示例的更多相关文章
- python网络编程05 /TCP阻塞机制
python网络编程05 /TCP阻塞机制 目录 python网络编程05 /TCP阻塞机制 1.什么是拥塞控制 2.拥塞控制要考虑的因素 3.拥塞控制的方法: 1.慢开始和拥塞避免 2.快重传和快恢 ...
- 《UNIX网络编程》TCP客户端服务器例子
最近在看<UNIX网络编程>(简称unp)和<Linux程序设计>,对于unp中第一个获取服务器时间的例子,实践起来总是有点头痛的,因为作者将声明全部包含在了unp.h里,导致 ...
- UNIX网络编程——解决TCP网络传输“粘包”问题
当前在网络传输应用中,广泛采用的是TCP/IP通信协议及其标准的socket应用开发编程接口(API).TCP/IP传输层有两个并列的协议:TCP和UDP.其中TCP(transport contro ...
- UNIX网络编程卷1 时间获取程序server TCP 协议相关性
本文为senlie原创.转载请保留此地址:http://blog.csdn.net/zhengsenlie 最初代码: 这是一个简单的时间获取server程序.它和时间获取程序client一道工作. ...
- 【Unix 网络编程】TCP 客户/服务器简单 Socket 程序
建立一个 TCP 连接时会发生下述情形: 1. 服务器必须准备好接受外来的连接.这通常通过调用 socket.bind 和 listen 这三个函数来完成,我们称之为被动打开. 2. 客户通过调用 c ...
- UNIX网络编程卷1 时间获取程序client TCP 使用非堵塞connect
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.当在一个非堵塞的 TCP 套接字(可使用 fcntl 把套接字变成非堵塞的)上调用 co ...
- UNIX网络编程——基本TCP套接字编程
一.基于TCP协议的网络程序 下图是基于TCP协议的客户端/服务器程序的一般流程: 服务器调用socket().bind().listen()完成初始化后,调用accept()阻塞等待,处于监听端口的 ...
- Unix 网络编程(2)——TCP API
TCP C/S套接口函数一般调用过程及基本函数 如上图所示的TCP连接的基本过程.一般来说,服务器先于客户端运行,服务器程序运行的基本过程是: socket()函数创建服务器段socket. bind ...
- UNIX网络编程——UDP回射服务器程序(初级版本)以及漏洞分析
该函数提供的是一个迭代服务器,而不是像TCP服务器那样可以提供一个并发服务器.其中没有对fork的调用,因此单个服务器进程就得处理所有客户.一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是 ...
- UNIX网络编程——Socket/TCP粘包、多包和少包, 断包
为什么TCP 会粘包 前几天,调试mina的TCP通信, 第一个协议包解析正常,第二个数据包不完整.为什么会这样吗,我们用mina这样通信框架,还会出现这种问题? TCP(transport cont ...
随机推荐
- Android优化应用启动速度
一.应用的启动 启动方式 通常来说,在安卓中应用的启动方式分为两种:冷启动和热启动. 1.冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动 ...
- 关于webpack,你想知道的都在这;
咱也标题党一回 哈哈哈 要使用webpack优化项目打包构建速度,首先得知道问题出在哪, 要知道问题出在哪,首先得知道webpack 打包的基本原理才能针对性的去做优化,下面首先了解webpack基本 ...
- ubuntu下安装typora、pycharm、搜狗拼音、MySQL、docker
安装typora # or run: # sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys BA300B7755AFCFAE ...
- WePY开发环境的安装和小程序生成WePY项目
相对于微信开发者工具而言,WePY的安装和生成项目稍显复杂.特记录下安装顺序: 1.安装Node.js 在Node官网(https://nodejs.org/)下载Node.js的安装包,此处我下载的 ...
- [已解决] 含gorm、sqlite3包的go程序构建失败 C:\Program Files\Go\pkg\tool\windows_amd64\link.exe: running gcc failed: exit status 1
gorm官方文档教程实例,构建出现错误.C:\Program Files\Go\pkg\tool\windows_amd64\link.exe: running gcc failed: exit st ...
- AcWing 1027. 方格取数(线性DP)
题目链接 题目描述 设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0.如下图所示: 某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B ...
- upsource 配置git仓库时的 rsa 问题
在使用 upsource 时,当 通过 SSH-key 需要配置一个 git 仓库代码时,在使用本机已有配置的 rsa 是出现无法连接的问题.这是需要看下具体的提示,如下图的显示 其实关键的地方看这个 ...
- SSM整合_年轻人的第一个增删改查_新增
写在前面 SSM整合_年轻人的第一个增删改查_基础环境搭建 SSM整合_年轻人的第一个增删改查_查找 SSM整合_年轻人的第一个增删改查_新增 SSM整合_年轻人的第一个增删改查_修改 SSM整合_年 ...
- 2021.11.30 eleveni的水省选题的记录
2021.11.30 eleveni的水省选题的记录 因为eleveni比较菜,eleveni决定先刷图论,再刷数据结构,同时每天都要刷dp.当然,对于擅长的图论,eleveni决定从蓝题开始刷.当然 ...
- 元素偏移量 offset 系列
offset 概述 offset翻译过来就是偏移量,我们使用offset系列相关属性可以动态的得到该元素的位置(偏移).大小等. 获得元素距离带有定位父元素的位置 获得元素自身的大小(宽度高度) 注意 ...