【1】基本 TCP 客户/服务器程序套接字执行流程
客户端的角度总结TCP客户/服务器
服务器的角度总结TCP客户/服务器
【2】SOCKET 程序设计常见常数说明
FAMILY常量
FAMILY | 说明 |
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | UNIX域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
TYPE常量
TYPE | 说明 |
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据包套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
PROTOCOL常量
PROTOCOL | 说明 |
IPPROTO_TCP | TCP传输协议 |
IPPROTO_UDP | UDP传输协议 |
IPPROTO_SCTP | SCTP传输协议 |
常量FAMILY与常量TYPE组合的意义
AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY | |
SOCK_STREAM | TCP|SCTP | TCP|SCTP | 是 | ||
SOCK_DGRAM | UDP | UDP | 是 | ||
SOCK_SEQPACKET | SCTP | SCTP | 是 | ||
SOCK_RAW | IPv4 | IPv6 | 是 | 是 |
【3】典型的TCP客户/服务器程序示例
客户端典型程序
#include "unp.h"
int
main(int argc, char **argv)
{
/**
* socket 套接字描述符
*/
int sockfd;
/**
* socket 套接字地址结构
*/
struct sockaddr_in servaddr;
//检查入参
if (argc != 2)
err_quit("usage: tcpcli ");
/**
* 新建套接字
* AF_INET : IPv4协议
* SOCK_STREAM : 字节流套接字
* 0 : 套接字所用的协议,此处并未指定
*/
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
/**
* 初始化套接字地址,此处指定端口默认为7
*/
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));
/**
* TCP 会射客户程序
*/
str_cli(stdin, sockfd); /* do it all */
exit(0);
}
服务器端典型函数
#include "unp.h"
int
main(int argc, char **argv)
{
/**
* Socket 套接字描述符
*/
int listenfd, connfd;
/**
* 子线程 ID
*/
pid_t childpid;
/**
* 对应于子进程的 Socket 描述符
*/
socklen_t clilen;
/**
* Socket 套接字地址
* 分别为客户端地址与服务器端地址
*/
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);
//新建服务器端套接字
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));
//监听套接字,该函数的 backlog 是由环境变量 LISTENQ 动态设置的
Listen(listenfd, LISTENQ);
/**
* 设置信号处理函数,此处处理信号 SIGCHLD
* SIGCHLD : 产生条件,一个进程终止或者停止时,该信号发送给父进程
* 可以通过捕获该信号,在相应的信号处理函数中完成对子进程的清理工作
*
* 此处设置该信号处理函数作用 : 用于清理僵死子进程
*/
Signal(SIGCHLD, sig_chld);
for ( ; ; ) {
clilen = sizeof(cliaddr);
//接收客户端连接
if ( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0) {
/**
* 处理 EINTR 错误,此处忽略 EINTR 错误
* 此处为了处理慢系统调用情况
* 慢系统调用规则: 当阻塞于某个慢系统调用的进程捕获某个信号且
* 相应的信号处理函数返回时,该系统可能返回 EINTR 错误
*/
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
/**
* 创建子进程
*/
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 */
}
}
各子函数
Signal 子函数 :
/**
* 原型: void (*signal(int signo, void (*func)(int)))(int)
* 函数分析:
* 函数名: signal
* 函数参数: int signo, void (*func)(int)
* 返回值类型: void (*)(int),相当于返回了一个函数
*
* 功能: 为signo指定的信号分配信号处理函数
**/
/**
* typedef 说明:
* typedef void Sigfunc(int);
* 此处制定了别名,即 void (int), 入参为int返回值为void的函数的别名为Sigfunc;
**/
Sigfunc *
Signal(int signo, Sigfunc *func) /* for our signal() function */
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR)
err_sys("signal error");
return(sigfunc);
}
err_sys 子函数 :
/**
* 输出错误日志信息
* 可变参数输入
**/
void
err_sys(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, LOG_ERR, fmt, ap);
va_end(ap);
exit(1);
}
str_echo 子函数 :
/**
* 服务器端回射处理程序
*/
void
str_echo(int sockfd)
{
ssize_t n;
//数据缓冲区
char buf[MAXLINE];
again:
//从网络中读取数据
while ( (n = read(sockfd, buf, MAXLINE)) > 0)
//将接收到的数据回写给客户端
Writen(sockfd, buf, n);
/**
* 检查信号类型,当检测到 EINTR 类型信号时,将重新调用 read 函数等待客户端数据
*
* 此处处理慢系统调用
* 适用于慢系统调用的基本规则是 : 当阻塞于某个慢系统调用的一个进程捕获某个信号
* 且相应的信号处理函数返回时,该系统调用可能返回一个 EINTR 错误
*/
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
//若读取过程中发生错误,则记录日志并退出
err_sys("str_echo: read error");
}
err_quit 子函数:
void
err_quit(const char *fmt, ...)
{
va_list ap;
// 用于获取可变参数列表
va_start(ap, fmt);
err_doit(0, LOG_ERR, fmt, ap);
// 用于清空可变参数列表
va_end(ap);
// 程序退出
exit(1);
}
Socket 子函数:
/**
* Socket 创建函数
* 此处进行了包裹,当创建 Socket 失败则打印日志记录;
*/
/* include Socket */
int
Socket(int family, int type, int protocol)
{
int n;
/**
* 原型 : SOCKET PASCAL FAR socket( int af, int type, int protocol);
*
* 参数 :
* af : 一个地址描述。目前仅支持AF_INET格式,也就是说ARPA Internet地址格式;
* type : 新套接口的类型描述;
* protocol : 套接字所用的协议。如调用者不想指定,可用0指定,表示缺省;
*
*/
if ( (n = socket(family, type, protocol)) < 0)
err_sys("socket error");
return(n);
}
/* end Socket */
Inet_pton 子函数 :
/**
* 套接字地址转换函数
* strptr : 地址的字符串表达
* addrptr : 地址的二进制转换结果
*
* 返回值 : 成功返回1; 若输入不是有效的表达式则返回0; 若出错则返回-1;
*/
void
Inet_pton(int family, const char *strptr, void *addrptr)
{
int n;
if ( (n = inet_pton(family, strptr, addrptr)) < 0)
//出错则记录错误日志并退出
err_sys("inet_pton error for %s", strptr); /* errno set */
else if (n == 0)
//出错则记录错误日志并退出
err_quit("inet_pton error for %s", strptr); /* errno not set */
/* nothing to return */
}
str_cli 子函数 :
/**
* TCP 回射客户端程序
*/
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)
//若接收到 EOF 的退出
err_quit("str_cli: server terminated prematurely");
//将接收到的数据输出到标准
Fputs(recvline, stdout);
}
}
【4】典型的TCP客户/服务器程序说明
并发服务器网络编程状态变迁图示
1. 正常启动
1. 服务器端调用socket,bind,listen,accept 并阻塞与 accept 调用;
2. 客户端调用 socket,connnect 引发 TCP 三次握手过程,建立连接;
3. 客户端调用 str_cli 函数并阻塞于 fgets 函数等待用户输入;
4. 服务器端 accept 函数返回,fork 子进程,由子进程调用 str_echo 函数,该函数阻塞于 read 函数调用等待客户端发送的数据,
父进程再次调用并阻塞于 accept 函数;
2. 正常终止
1. 客户端数据输入遇到 EOF 字符,fgets 返回一个空指针,str_cli 函数返回;
2. main 函数继续执行,调用 exit 函数终止客户端进程;
3. 终止处理关闭所有打开的描述符,客户端向服务器发送 FIN ,服务器响应一个 ACK ,
此后客户端处于 FIN_WAIT_2 状态,服务器端处于 CLOSE_WAIT状态;
4. 当服务器 TCP 接受到 FIN 时,服务器子进程阻塞于 readline 调用,此时 readline 返回0,
str_echo 函数返回服务器子进程 main 函数,mian 函数调用 exit 终止进程;
5. 服务器子进程中关闭打开的描述符,向客户端发送 FIN ,客户端响应 ACK,此时连接完全终止,
客户端处于 TIME_WAIT状态;
6. 子进程终止时,向父进程发送 SIGCHLD 信号,该信号的默认行为是忽略信号处理,子进程进入僵死状态;
3. 异常终止
(1). accept 返回前连接中止
对于上述终止现象,POSIX 将会返回值为 ECONNABORTED 的 errno,服务器通常忽略 ECONNABORTED 错误,并重新调用 accept;
(2). 服务器进程终止
1. 服务器子进程终止时关闭子进程所有打开的描述符,服务器向客户端发送 FIN,客户端响应 ACK;
2. 服务器端子进程终止时向父进程发送 SIGCHLD 信号;
3. 客户端接收到 FIN之后向服务器响应 ACK,阻塞于 fgets 调用;
4. 客户端接收到输入数据后向服务器发送数据,由于服务器端子进程终止,服务器响应 RST;
5. 客户端阻塞于 readline,由于步骤1中服务器向客户端发送了 FIN,则 read返回0(EOF),
客户端提示出错信息"server terminated prematurely"并退出;
6. 客户端终止关闭所有描述符;
注: 若客户端忽视 readline 返回的 EOF 错误,继续向服务器发送数据,此时处理规则如下:
当一个进程向某个已收到 RST 的套接字执行写操作时,内核会向该进程发送 SIGPIPE 信号,
该信号的默认行为是终止进程,此时写操作返回 EPIPE 错误;
(3). 服务器主机崩溃
1. 当服务器主机崩溃,已有的网络连接不会发送任何数据;
2. 客户端接收输入后发送数据并阻塞于 readline,等待服务器应答;
3. 此时客户端会不断重传数据,直到 TIMEOUT并返回一个错误;
ETIMEDOUT:服务器崩溃对客户端数据分解没有应答;
EHOSTUNREACH,ENETUNREACH:中间路由判断服务器不可达;
(4). 服务器崩溃后重启
1. 服务器崩溃后重启,其TCP丢失了崩溃前的所有连接信息,服务器 TCP对所收到的客户的数据分解响应RST;
2. 客户端 TCP 接收到RST时,客户端阻塞于 readline调用,导致调用返回 ECONNRESET;
【5】慢系统调用与快系统调用
慢系统调用 : 该系统调用在某些外部事件发生之前不会返回;
快系统调用 : 也称非阻塞,该系统调用立即返回;
“立即”意味着此类系统调用只需要一点处理器时间; 此类系统调用可以持续多长时间(除了实时系统)没有硬性限制,但是这些调用一经预定足够长时间就会返回;
参考
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。
【1】UNIX网络编程
【2】慢速系统调用(slow system calls)和快速系统调用(fast system calls)之间的区别