【Linux网络编程】基本 TCP 套接字编程

【Linux网络编程】基本 TCP 套接字编程

【1】基本 TCP 客户/服务器程序套接字执行流程

【Linux网络编程】基本 TCP 套接字编程_第1张图片

客户端的角度总结TCP客户/服务器

【Linux网络编程】基本 TCP 套接字编程_第2张图片

服务器的角度总结TCP客户/服务器

【Linux网络编程】基本 TCP 套接字编程_第3张图片

【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客户/服务器程序说明

并发服务器网络编程状态变迁图示

【Linux网络编程】基本 TCP 套接字编程_第4张图片

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 返回前连接中止

【Linux网络编程】基本 TCP 套接字编程_第5张图片

对于上述终止现象,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)之间的区别

你可能感兴趣的:(网络编程)