《TCP/IP 网络编程》笔记

本文主要基于《TCP/IP 网络编程》这本书进行总结,主要针对 Linux 网络编程部分进行阐述,Windows 网络编程部分有需要建议阅读原书籍。

一、基础知识

网络编程

网络编程就是编写程序使两台连网的计算机相互交换数据。

套接字

套接字是网络数据传输用的软件设备。我们把插头插到插座上就能从电网获得电力供给,同样为了与远程计算机进行数据传输, 需要连接到因特网,而编程中的"套接字"就是用来连接该网络的工具。它本身就带有"连接"的含义,如果将其一引申, 则还可以表示两台计算机之间的网络连接。

#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 成功时返回文件描述符,失败时返回-1
/* 
   domain 套接字中使用的协议族( Protocol Famjly )信息。
   type 套接字数据传输类型信息。
   protocol 计算机间通信中使用的协议信息。
*/
协议族 domain:
  • PF_INET IPv4互联网协议族

  • PF_INET6 IPv6互联网协议族

  • PF_LOCAL 本地通信的UNIX协议族

  • PF_PACKET 底层套接字的协议族

  • PF_IPX IPX Novel1协议族

套接字类型 type
  • 面向连接的套接字 SOCK_STREAM(TCP)

  • 面向消息的套接字 SOCK_DGRAM (UDP)

协议的最终选择 protocol

传递前两个参数即可创建所需套接字,大部分情况下可以向第三个参数传递0,除非同一协议族中存在多个数据传输方式相同的协议

// IPv4协议族中面向连接的套接字
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

// IPv4协议族中面向消息的套接字
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

基于 Linux 的文件操作

对于 Linux 而言,socket 操作和文件操作没有区别,文件描述符就是系统分配给文件或套接字的整数。

打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path , int flag);
// 成功时返回文件描述符,失败时返回-1
// path 文件名的字符串地址。 flag 文件打开模式信息。
关闭文件
#include <unistd.h>

int close(int fd);
// 成功时返回0,失败时返回-1
// fd 需要关闭的文件或套接字的文件描述符
写入文件
#include <unistd.h>

ssize_t write(int fd, const void* buf, size_t nbytes);
// 成功时返回写入的字节数,失败时返回-1
// fd 显示数据传输对象的文件描述符   buf 保存要传输数据的缓冲地址值   nbytes 要传输数据的字节数
读取文件
#include <unistd.h>

ssize_t read(int fd, void * buf, size_t nbytes);
// 成功时返回接收的字节数(但遇到文件结尾则返回0),失败时返回-1
// fd 显示数据接收对象的文件描述符   buf 保存要接收数据的缓冲地址值   nbytes 要接收数据的最大字节数

地址信息的表示与分配

通过结构体 sockaddr_in 将地址信息传入 bind 函数中

struct sockaddr_in
{
	sa_family_t    sin_family;     //地址族( Address Family )
	uint16_t       sin_port;       //16位 TCP/UDP 端口号
	struct in_addr sin_addr;       //32位 IP 地址
	char           sin_zero[8];    //不使用
}

struct in_addr
{
    In_add_t       s_addr;         //32位 IPv4 地址
}

套接字创建过程中常见的网络地址信息初始化方法。

struct sockaddr_in addr;
char* serv_ip = "211.117.168.13";              //声明 IP 地址字符串
char* serv_port = "9190";                      //声明端口号字符串
memset(&addr, 0, sizeof(addr));                // 结构体变量 addr 的所有成员初始化为0
addr.sin_family = AF_INET;                     //指定地址族
// inet_addr 将字符串信息转换为网络字节序的整数型
addr.sin_addr.s_addr = inet_addr(serv_ip);     //基于字符串的 IP 地址初始化
// addr.sin_addr.s_addr = htonl(INADDR_ANY); 若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。
// htons 将主机字节序转换为网络字节序(大端序) s表示short型   l表示long型
addr.sin_port = htons(atoi(serv_port));        //基于字符串的端口号初始化

把初始化的地址信息分配给套接字。bind 函数负责这项操作。

#include <sys/socket.h>

int bind(int sockfd , struct sockaddr* myaddr, socklen_t addrlen);
// 成功时返回0 ,失败时返回-1
/* 
   sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符。
   myaddr 存有地址信息的结构体变量地址值。
   addrlen 第二个结构体变量的长度。
*/

进入等待连接请求状态

调用 listen 函数转为可接收请求状态。只有调用了listen函数,客户端才能进入可发出连接请求的状态。

#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 成功时返回0 ,失败时返回-1
/* 
	sock     希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字) 。
	backlog  连接请求等待队列(Queue)的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列。
*/

受理客户端连接请求

调用 accept 函数受理连接请求。accept 函数受理连接请求等待队列中待处理的客户端连接请求。函数调用成功时, accept 函数内部将产生用于数据 I/O 的套接字, 并返回其文件描述符。

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 成功时返回文件描述符,失败时返回-1
/*
	sock      服务器套接字的文件描述符。
	addr      保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
	addrlen   第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。
*/

Hello World 服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

	char message[]="Hello World!";
	
	if(argc!=2){
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	// 创建套接字
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	if(serv_sock == -1)
		error_handling("socket() error");
	// 初始化地址信息
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_addr.sin_port=htons(atoi(argv[1]));
	// 将地址分配给套接字
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
		error_handling("bind() error"); 
	// 进入等待连接请求状态
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	// 受理客户端连接请求
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
	if(clnt_sock==-1)
		error_handling("accept() error");  
	// 通过 write 函数向客户端传输数据
	write(clnt_sock, message, sizeof(message));
    // 调用 close 函数关闭连接
	close(clnt_sock);	
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Hello World 客户端

客户端通过 connect 函数请求连接

#include <sys/socket.h>

int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);
// 成功时返回0 ,失败时返回-1
/*
	sock        客户端套接字文件描述符。
	servaddr    保存目标服务器端地址信息的变量地址值
	addrlen     以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
*/

完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
	char message[30];
	int str_len;
	
	if(argc!=3){
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	// 创建套接字
	sock=socket(PF_INET, SOCK_STREAM, 0);
	if(sock == -1)
		error_handling("socket() error");
	// 初始化地址信息 初始化值为目标服务器端套接字的IP和端口信息。
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));
	// 请求连接
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
		error_handling("connect() error!");
	// 接收服务端传输的数据
	str_len=read(sock, message, sizeof(message)-1);
	if(str_len==-1)
		error_handling("read() error!");
	
	printf("Message from server: %s \n", message);  
    // 关闭连接
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

图片为函数调用关系

二、基于TCP的回声服务

回声( echo ) 服务器端/客户端就是将客户端传输的字符串数据原封不动地传回客户端。

基本运行方式:

  • 服务器端在同一时刻只与一个客户端相连,并提供回声服务。

  • 服务器端依次向5个客户端提供服务并退出。

  • 客户端接收用户输入的字符串并发送到服务器端。

  • 服务器端将接收的字符串数据传回客户端,即"回声" 。

  • 服务器端与客户端之间的字符串回声一直执行到客户端输入Q为止。

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;
	
	struct sockaddr_in serv_adr;
	struct sockaddr_in clnt_adr;
	socklen_t clnt_adr_sz;
	
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(serv_sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_adr_sz=sizeof(clnt_adr);

	for(i=0; i<5; i++)
	{
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		if(clnt_sock==-1)
			error_handling("accept() error");
		else
			printf("Connected client %d \n", i+1);
	
		while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
			write(clnt_sock, message, str_len);

		close(clnt_sock);
	}

	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;
		// 这里存在问题,TCP不存在数据边界,多次调用 write 函数传递的字符串有可能一次性传递到服务器端此时客户端有可能从服务		器端收到多个字符串服务器端希望通过调用1 次write函数传输数据,但如果数据太大,操作系统就有可能把数据分成多个数据包发          送到客户端。另外,在此过程中,客户端有可能在尚未收到全部数据包时就调用read函数。
		//write(sock, message, strlen(message));
		//str_len=read(sock, message, BUF_SIZE-1);
        // 以上代码改为下面的
        recv_len=0;
		while(recv_len<str_len)
		{
			recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
			if(recv_cnt==-1)
				error_handling("read() error!");
			recv_len+=recv_cnt;
		}
		message[str_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

三、基于UDP的回声服务

UDP 提供的是不可靠的数据传输服务,但是在传输速度上要优于 TCP。更多 UDP 和 TCP 的细节请参考《计算机网络-自顶向下》。

UDP 中的服务器端和客户端没有连接,不必调用TCP连接过程中调用的 listen 函数和 accept 函数,只有创建套接字的过程和数据交换过程。

基于 UDP 的数据 I/O 函数

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
// 成功时返回传输的字节数, 失败时返回-1。
/*
	sock		用于传输数据的UDP套接字文件描述符。
	buff		保存待传输数据的缓冲地址值。
	nbytes		待传输的数据长度,以字节为单位。
	flags		可选项参数,若没有则传递0。
	to			存有目标地址信息的sockaddr结构体变量的地址值。
	addrlen		传递给参数to的地址值结构体变量长度。
*/
#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
// 成功时返回接收的字节数, 失败时返回-1。
/*
	sock		用于接收数据的UDP套接字文件描述符。
	buff		保存接收数据的缓冲地址值。
	nbytes		可接收的最大字节数,故无法超过参数bu忏所指的缓冲大小。
	flags		可选项参数,若没有则传递0。
	from		存有发送端地址信息的sockaddr结构体变量的地址值。
	addrlen		保存参数from的结构体变量长度的变量地址值。
*/

基于 UDP 的回声客户端

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	char message[BUF_SIZE];
	int str_len;
	socklen_t clnt_adr_sz;
	
	struct sockaddr_in serv_adr, clnt_adr;
	if(argc!=2){
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	// 创建UDP套接字
	serv_sock=socket(PF_INET, SOCK_DGRAM, 0);
	if(serv_sock==-1)
		error_handling("UDP socket creation error");
	// 初始化地址信息
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	// 将地址分配给套接字
	if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");

	while(1) 
	{
		clnt_adr_sz=sizeof(clnt_adr);
        // 利用分配的地址接收数据
		str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
								(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        // 上一行函数调用同时获取数据传输端的地址,利用该地址将数据回传
		sendto(serv_sock, message, str_len, 0, 
								(struct sockaddr*)&clnt_adr, clnt_adr_sz);
	}	
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len;
	socklen_t adr_sz;
	
	struct sockaddr_in serv_adr, from_adr;
	if(argc!=3){
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	// 创建UDP套接字
	sock=socket(PF_INET, SOCK_DGRAM, 0);   
	if(sock==-1)
		error_handling("socket() error");
	// 初始化地址信息
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	while(1)
	{
		fputs("Insert message(q to quit): ", stdout);
		fgets(message, sizeof(message), stdin);     
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))	
			break;
		// 向服务端传输数据
		sendto(sock, message, strlen(message), 0, 
					(struct sockaddr*)&serv_adr, sizeof(serv_adr));
        // j
		adr_sz=sizeof(from_adr);
		str_len=recvfrom(sock, message, BUF_SIZE, 0, 
					(struct sockaddr*)&from_adr, &adr_sz);

		message[str_len]=0;
		printf("Message from server: %s", message);
	}	
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

四、多进程服务器端

进程

进程定义为“占用内存空间的正在运行的程序”

通过调用 fork 函数创建进程,fork 函数将创建调用的进程副本

#include <unistd.h>

pid_t fork(void);
// 成功时返回进程ID,失败时返回-1

fork 函数使用示例:

#include <stdio.h>
#include <unistd.h>
int gval=10;

int main(int argc, char *argv[])
{
	pid_t pid;
	int lval=20;
	gval++, lval+=5;
	
	pid=fork();		
	if(pid==0)	// if Child Process
		gval+=2, lval+=2;
	else			// if Parent Process
		gval-=2, lval-=2;
	
	if(pid==0)
		printf("Child Proc: [%d, %d] \n", gval, lval);
	else
		printf("Parent Proc: [%d, %d] \n", gval, lval);
	return 0;
}

僵尸进程

进程完成工作后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作"僵尸进程"

如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存, 并让子进程长时间处于僵尸进程状态。

为了销毁子进程,父进程应主动请求获取子进程的返回值。销毁僵尸进程方法:

  • 利用 wait 函数:调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值

  • wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int * statloc, int options);
// 成功时返回终止的子进程ID(或0),失败时返回-1
/*
	pid			等待终止的目标子进程的10 ,若传递- 1 ,则与wait函数相同,可以等待任意子进程终止。
	statloc		与wait函数的statloc参数具有相同含义。
	options		传递头文件中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。
*/

信号处理

子进程究竟何时终止?调用 waitpid 函数后要无休止地等待吗?如果操作系统可以在子进程终止时向父进程传递信号,让父进程转而处理子进程终止的相关事宜,此处的"信号"是在特定事件发生时由操作系统向进程发送的消息。

信号注册函数

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);
// 为了在产生信号时调用,返回之前注册的函数指针

信号处理示例

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
    // 已到通过调用alarm函数注册的时间。
	if(sig==SIGALRM)
		puts("Time out!");
	alarm(2);	
}
void keycontrol(int sig)
{
    // 输入CTRL+C
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[])
{
	int i;
	signal(SIGALRM, timeout);
	signal(SIGINT, keycontrol);
	alarm(2);

	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}

相比于 signal 函数,sigaction 函数更稳定,使用更多

#include <signal.h>

int sigaction(int signo, const struct sigaction * act, struct sigaction *oldact);
// 成功时返回0,失败时返回-1
/*
	signo		与signal函数相同,传递信号信息。
	act			对应于第一个参数的信号处理函数(信号处理器) 信息。
	oldact		通过此参数获取之前注册的信号处理函数指针,若不需要则传递0。
*/
// 声明并初始化sigaction结构体变量以调用上述函数
struct sigaction
{
    // 信号处理函数的指针值
    void (*sa_handler)(int);
    // sa-mask和sa_flags的所有位均初始化为0即可,这2个成员用于指定信号相关的选项和特性
    sigset_t sa_mask;
    int sa_flags;
}

信号处理示例:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig==SIGALRM)
		puts("Time out!");
	alarm(2);	
}

int main(int argc, char *argv[])
{
	int i;
	struct sigaction act;
	act.sa_handler=timeout;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGALRM, &act, 0);

	alarm(2);

	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}

利用信号处理消灭僵尸进程

子进程终止时将产生 SIGCHLD 信号,具体代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void read_childproc(int sig)
{
	int status;
	pid_t id=waitpid(-1, &status, WNOHANG);
	if(WIFEXITED(status))
	{
		printf("Removed proc id: %d \n", id);
		printf("Child send: %d \n", WEXITSTATUS(status));
	}
}

int main(int argc, char *argv[])
{
	pid_t pid;
	struct sigaction act;
	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGCHLD, &act, 0);

	pid=fork();
	if(pid==0)
	{
		puts("Hi! I'm child process");
		sleep(10);
		return 12;
	}
	else
	{
		printf("Child proc id: %d \n", pid);
		pid=fork();
		if(pid==0)
		{
			puts("Hi! I'm child process");
			sleep(10);
			exit(24);
		}
		else
		{
			int i;
			printf("Child proc id: %d \n", pid);
			for(i=0; i<5; i++)
			{
				puts("wait...");
				sleep(5);
			}
		}
	}
	return 0;
}
/*
root@my_linux:/home/swyoon/tcpip# gcc remove_zombie.c -o zombie
root@my_linux:/home/swyoon/tcpip# ./zombie
Hi! I'm child process
Child proc id: 9529 
Hi! I'm child process
Child proc id: 9530 
wait...
wait...
Removed proc id: 9530 
Child send: 24 
wait...
Removed proc id: 9529 
Child send: 12 
wait...
wait...
*/

基于多任务的并发服务器

每当有客户端请求服务(连接请求)时,回声服务器端都创建子进程以提供服务。请求服务的客户端若有5个,则将创建5个子进程提供服务。为了完成任务,需要如下阶段:

  • 第一阶段: 回声服务器端(父进程)通过调用 accept 函数受理连接请求。

  • 第二阶段: 此时获取的套接字文件描述符创建并传递给子进程。

  • 第三阶段: 子进程利用传递来的文件描述符提供服务。

服务端代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	
	pid_t pid;
	struct sigaction act;
	socklen_t adr_sz;
	int str_len, state;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	// sigaction结构体初始化及信号处理函数注册
	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	state=sigaction(SIGCHLD, &act, 0);
    
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	while(1)
	{
		adr_sz=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
		if(clnt_sock==-1)
			continue;
		else
			puts("new client connected...");
        // 创建子进程
		pid=fork();
		if(pid==-1)
		{
			close(clnt_sock);
			continue;
		}
        // 子进程运行
		if(pid==0)
		{
            // 复制套接字后同一端口将对应多个套接字。调用fork函数后,要将无关的套接字文件描述符关掉,子进程关闭服务端套接字
			close(serv_sock);
			while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
				write(clnt_sock, buf, str_len);
			
			close(clnt_sock);
			puts("client disconnected...");
			return 0;
		}
		else
            // 父进程关闭客户端连接套接字
			close(clnt_sock);
	}
	close(serv_sock);
	return 0;
}

void read_childproc(int sig)
{
	pid_t pid;
	int status;
	pid=waitpid(-1, &status, WNOHANG);
	printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

I/O 分割的客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);

int main(int argc, char *argv[])
{
	int sock;
	pid_t pid;
	char buf[BUF_SIZE];
	struct sockaddr_in serv_adr;
	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0);  
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_adr.sin_port=htons(atoi(argv[2]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("connect() error!");
	// 子进程用于写数据,父进程用于读数据
	pid=fork();
	if(pid==0)
		write_routine(sock, buf);
	else 
		read_routine(sock, buf);

	close(sock);
	return 0;
}

void read_routine(int sock, char *buf)
{
	while(1)
	{
		int str_len=read(sock, buf, BUF_SIZE);
		if(str_len==0)
			return;

		buf[str_len]=0;
		printf("Message from server: %s", buf);
	}
}
void write_routine(int sock, char *buf)
{
	while(1)
	{
		fgets(buf, BUF_SIZE, stdin);
		if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
		{	
			shutdown(sock, SHUT_WR);
			return;
		}
		write(sock, buf, strlen(buf));
	}
}
void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

五、基于I/O复用的服务器端

创建进程需要大量的运算和内存空间,不同进程间的数据交换也相对复杂,因此 I/O复用技术就应运而生。

select 函数

#include <sys/select.h>
#include <sys/time . h>

int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);
// 成功时返回大于0的值,失败时返回-1。
/*
	maxfd			监视对象文件描述符数量。
	readset			将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
	writeset		将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
	exceptset		将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,并传递其地址值。
	timeout			调用select函数后,为防止陷入无限阻塞的状态,传递超时( time-out )信息。
	返回值			 发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
*/

使用select函数时可以将多个文件描述符集中到一起统一监视。select函数的调用方法和顺序如下图

设置文件描述符利用 fd_set 数组变量完成,在 fd_set 变量中注册或更改值的操作都由下列宏完成:

  • FD_ZERO(fd_set * fdset) : 将 fd_set 变量的所有位初始化为0 。

  • FD_SET(int fd, fd_set * fdset): 在参数 fdset 指向的变量中注册文件描述符 fd 的信息。

  • FD_ CLR(int fd, fd_set * fdset): 从参数 fdset 指向的变量中清除文件描述符fd的信息。

  • FD_ISSET(int fd, fd_set * fdset) : 若参数fdset指向的变量中包含文件描述符fd的信息, 则返回真。

设置检查(监视)范围及超时:

  • 文件描述符的监视范围与 select 函数的第一个参数有关。只需将最大的文件描述符值加1再传递到 select 函数即可。

  • select 函数的超时时间与 select 函数的最后一个参数有关,其中 timeval 结构体定义如下

struct timeval
{
    long tv_sec;   // seconds
    long tv_usec;  // microseconds
}

调用 select 函数后查看结果

select 函数调用完成后,向其传递的 fd_set 变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外,因此可以认为值仍为1 的位置上的文件描述符发生了变化。

select 函数调用示例:

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>

#define BUF_SIZE 30

int main(int argc, char *argv[])
{
	fd_set reads, temps;
	int result, str_len;
	char buf[BUF_SIZE];
	struct timeval timeout;
	// 设置文件描述符
	FD_ZERO(&reads);
	FD_SET(0, &reads); // 0 is standard input(console)

	/*
	timeout.tv_sec=5;
	timeout.tv_usec=5000;
	*/

	while(1)
	{
		temps=reads;
        // 设置超时时间
		timeout.tv_sec=5;
		timeout.tv_usec=0;
        // 调用select函数
		result=select(1, &temps, 0, 0, &timeout);
		if(result==-1)
		{
			puts("select() error!");
			break;
		}
		else if(result==0)
		{
			puts("Time-out!");
		}
		else 
		{
            // 验证发生变化的文件描述符是否为标准输入,若是,则从标准输入读取数据并向控制台输出。
			if(FD_ISSET(0, &temps)) 
			{
				str_len=read(0, buf, BUF_SIZE);
				buf[str_len]=0;
				printf("message from console: %s", buf);
			}
		}
	}
	return 0;
}
/*
root@my_linux:/tcpip# gcc select.c -o select
root@my_linux:/tcpip# ./select
Hi-
message from console: Hi-
Hello-
message from console: Hello-
Time-out!
Time-out!
Good bye-
message from console: Good bye-
*/

实现 I/O复用服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>

#define BUF_SIZE 100
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	struct timeval timeout;
	fd_set reads, cpy_reads;

	socklen_t adr_sz;
	int fd_max, str_len, fd_num, i;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	FD_ZERO(&reads);			// 初始化文件描述符
	FD_SET(serv_sock, &reads);	// 注册服务器端套接字
	fd_max=serv_sock;

	while(1)
	{
		cpy_reads=reads;
		timeout.tv_sec=5;
		timeout.tv_usec=5000;
		// 调用select函数
		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
			break;
		
		if(fd_num==0)
			continue;
		// 查找发生状态变化的文件描述符
		for(i=0; i<fd_max+1; i++)
		{
			if(FD_ISSET(i, &cpy_reads))
			{
                // 判断是否为服务器端套接字
				if(i==serv_sock)     // connection request!
				{
					adr_sz=sizeof(clnt_adr);
					clnt_sock=
						accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads);
					if(fd_max<clnt_sock)
						fd_max=clnt_sock;
					printf("connected client: %d \n", clnt_sock);
				}
				else    // read message!
				{
					str_len=read(i, buf, BUF_SIZE);
					if(str_len==0)    // close request!
					{
						FD_CLR(i, &reads);
						close(i);
						printf("closed client: %d \n", i);
					}
					else
					{
						write(i, buf, str_len);    // echo!
					}
				}
			}
		}
	}
	close(serv_sock);
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

优于 select 的 epoll

基于select 的 I/O复用技术速度慢的原因:

  • 调用 select 函数后常见的针对所有文件描述符的循环语句。

  • 每次调用 select 函数时都需要向该函数传递监视对象信息。

实现 epoll 所必要的函数和结构体
  • epoll_create 向操作系统请求创建保存文件描述符的空间

#include <sys/epoll.h>

int epoll_create(int size);
// 成功时返回epoll文件描述符,失败时返回-1。
  • epoll_ctl 生成 epoll 例程后,应在其内部注册监视对象文件描述符

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
// 成功时返回0 ,失败时返回-1
/*
	epfd	用于注册监视对象的epoll例程的文件描述符。
	op		用于指定监视对象的添加、删除或更改等操作。
			EPOLL_CTL_ADD  EPOLL_CTL_DEL  EPOLL_CTL_MOD  
	fd		需要注册的监视对象文件描述符。
	event	监视对象的事件类型。
			EPOLLIN: 需要读取数据的情况。
			EPOLLOUT: 输出缓冲为空,可以立即发送数据的情况。
			EPOLLPRI: 收到OOB数据的情况。
			EPOLLRDHUP: 断开连接或半关闭的情况,这在边缘触发方式下非常有用。
			EPOLLERR: 发生错误的情况。
			EPOLLET: 以边缘触发的方式得到事件通知。
			EPOLLONESHOT: 发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递							EPOLL_CTL_MOD,再次设置事件。
*/
  • epoll_wait 与 select 函数对应

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
// 成功时返回发生事件的文件描述符数,失败时返回-1
/*
	epfd		表示事件发生监视范围的epoll例程的文件描述符。
	events		保存发生事件的文件描述符集合的结构体地址值。
	maxevents	第二个参数中可以保存的最大事件数。
	timeout		以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
*/
基于 epoll 的回声服务器端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	event.events=EPOLLIN;
	event.data.fd=serv_sock;	
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1)
	{
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock)
			{
				adr_sz=sizeof(clnt_adr);
				clnt_sock=
					accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				event.events=EPOLLIN;
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{
					str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
					if(str_len==0)    // close request!
					{
						epoll_ctl(
							epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else
					{
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}
	
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}
条件触发和边缘触发

条件触发方式中, 只要输入缓冲中还剩有数据,就将以事件方式再次注册。epoll 默认以条件触发方式工作。

边缘触发中输入缓冲收到数据时仅注册1 次该事件。即使输入缓冲中还留有数据,也不会再进行注册。

基于边缘触发的回声服务器端

边缘触发方式中,接收数据时仅注册1次该事件,因此需要验证输入缓冲是否为空;边缘触发方式下,以阻塞方式工作的 read & write 函数有可能引起服务器端的长时间停顿,因此需要采用非阻塞方式。

将套接字改为非阻塞方式的方法:

#include <fcntl.h>

int fcntl(int filedes, int cmd, . . . );
// 成功时返回cmd参数相关值,失败时返回-1
/*
	filedes		属性更改目标的文件描述符。
	cmd			表示函数调用的目的。
*/

// 将文件(套接字)改为非阻塞模式,如下两句
// int flag = fcntl(fd, F_GETFL, 0);
// fcntl(fd, F_SETFL, flag|O_NONBLOCK);

以边缘触发方式工作的回声服务器端示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4		//为了验证边缘触发的工作方式,将缓冲设置为4字节。
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	setnonblockingmode(serv_sock);		// 将套接字改为非阻塞模式
	event.events=EPOLLIN;
	event.data.fd=serv_sock;	
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1)
	{
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		puts("return epoll_wait");		// 为观察事件发生数而添加的输出字符串的语句。
		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock)
			{
				adr_sz=sizeof(clnt_adr);
				clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				setnonblockingmode(clnt_sock);		// 将套接字改为非阻塞模式
				event.events=EPOLLIN|EPOLLET;		// 添加EPOLLET标志,将套接字事件注册方式改为边缘触发。
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{
					while(1)
					{
						str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
						if(str_len==0)    // close request!
						{
							epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
							close(ep_events[i].data.fd);
							printf("closed client: %d \n", ep_events[i].data.fd);
							break;
						}
						else if(str_len<0)
						{
							if(errno==EAGAIN)
								break;
						}
						else
						{
							write(ep_events[i].data.fd, buf, str_len);    // echo!
						}
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void setnonblockingmode(int fd)
{
	int flag=fcntl(fd, F_GETFL, 0);
	fcntl(fd, F_SETFL, flag|O_NONBLOCK);
}
void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

边缘触发方式可以分离接收数据和处理数据的时间点,从实现模型的角度看,边缘触发更有可能带来高性能。

六、多线程服务器端

线程

为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入了线程( Thread )。相比于进程有如下优点:

  • 线程的创建和上下文切换比进程的创建和上下文切换更快。

  • 线程间交换数据时无需特殊技术。

多个线程共享数据区和堆区,如果说进程在操作系统内部生成多个执行流,那么线程就在同一进程内部创建多条执行流。

线程的创建和执行

线程创建函数

#include <pthread.h>

int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
// 成功时返回0, 失败时返回其他值。
/*
	thread			保存新创建线程ID的变量地址值。线程与进程相同,也需要用于区分不同线程的ID
	attr			用于传递线程属性的参数,传递NULL时,创建默认属性的线程。
	start_routine	 相当于线程main 函数的飞在单独执行流中执行的函数地址值(函数指针) 。
	arg				通过第三个参数传递调用函数时包含传递参数信息的变量地址值。
*/

如何准确预测出 thread_main 函数的运行时间,并让 mian 函数恰好等待这么长时间呢?通常利用下面的函数控制线程的执行流。

#include <pthread.h>

int pthread_join(pthread_t thread, void **status);
// 成功时返回0,失败时返回其他值。
/*
	thread		该参数值ID的线程终止后才会从该函数返回。
	status		保存线程的main函数返回值的指针变量地址值。
*/

创建多个线程示例,下图为程序执行流程图

#include <stdio.h>
#include <pthread.h>
void * thread_summation(void * arg); 

int sum=0;

int main(int argc, char *argv[])
{
	pthread_t id_t1, id_t2;
	int range1[]={1, 5};
	int range2[]={6, 10};
	
	pthread_create(&id_t1, NULL, thread_summation, (void *)range1);
	pthread_create(&id_t2, NULL, thread_summation, (void *)range2);

	pthread_join(id_t1, NULL);
	pthread_join(id_t2, NULL);
	printf("result: %d \n", sum);
	return 0;
}

void * thread_summation(void * arg) 
{
	int start=((int*)arg)[0];
	int end=((int*)arg)[1];

	while(start<=end)
	{
		sum+=start;
		start++;
	}
	return NULL;
}

线程的问题和临界区

多个线程访问同一变量时存在同步的问题。

  • 2个线程同时执行函数,会构成临界区。

  • 2个线程分别执行不同的函数,也有可能构成临界区。

线程同步

  • 互斥量:不允许多个线程同时访问,通过加锁的方式

#include <pthread.h>
// 互斥量的创建和销毁
int pthread_mutex_init (pthread_mutex_t * mutex, const pthread_mutexattr_t * attr);
int pthread_mutex_destroy (pthread_mutex_t * mutex);
// 成功时返回0,失败时返回其他值。

// 互斥量锁住和释放临界区
int pthread_mutex_lock (pthread_mutex_t * mutex);
int pthread_mutex_unlock (pthread_mutex_t * mutex); // 如果忘记调用则会产生死锁
// 成功时返回0,失败时返回其他值。
  • 信号量:利用"二进制信号量" ( 只用0和1 )完成"控制线程顺序"为中心的同步方法

#include <semaphore.h>

int sem_init (sem_t * sem, int pshared, unsigned int value);
int sem_destroy (sem_ t * sem);
// 成功时返回0,失败时返回其他值。

int sem_post(sem_t * sem);
int sem_wait(sem_t * sem);
// 成功时返回0,失败时返回其他值。
// 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait时信号量减1

线程销毁

之前调用过 pthread_join 函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通常通过如下函数调用引导线程销毁。

#include <pthread.h>

int pthread_detach(pthread_t thread);
// 成功时返回0,失败时返回其他值。

多线程并发服务器端

这里介绍多个客户端之间可以交换信息的简单的聊天程序

服务器端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>

#define BUF_SIZE 100
#define MAX_CLNT 256

void * handle_clnt(void * arg);
void send_msg(char * msg, int len);
void error_handling(char * msg);

int clnt_cnt=0;
int clnt_socks[MAX_CLNT];
pthread_mutex_t mutx;

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	int clnt_adr_sz;
	pthread_t t_id;
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
  
	pthread_mutex_init(&mutx, NULL);
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);

	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET; 
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	while(1)
	{
		clnt_adr_sz=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz);
		
		pthread_mutex_lock(&mutx);
		clnt_socks[clnt_cnt++]=clnt_sock;
		pthread_mutex_unlock(&mutx);
	
		pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
		pthread_detach(t_id);
		printf("Connected client IP: %s \n", inet_ntoa(clnt_adr.sin_addr));
	}
	close(serv_sock);
	return 0;
}
	
void * handle_clnt(void * arg)
{
	int clnt_sock=*((int*)arg);
	int str_len=0, i;
	char msg[BUF_SIZE];
	
	while((str_len=read(clnt_sock, msg, sizeof(msg)))!=0)
		send_msg(msg, str_len);
	
	pthread_mutex_lock(&mutx);
	for(i=0; i<clnt_cnt; i++)   // remove disconnected client
	{
		if(clnt_sock==clnt_socks[i])
		{
			while(i++<clnt_cnt-1)
				clnt_socks[i]=clnt_socks[i+1];
			break;
		}
	}
	clnt_cnt--;
	pthread_mutex_unlock(&mutx);
	close(clnt_sock);
	return NULL;
}
void send_msg(char * msg, int len)   // send to all
{
	int i;
	pthread_mutex_lock(&mutx);
	for(i=0; i<clnt_cnt; i++)
		write(clnt_socks[i], msg, len);
	pthread_mutex_unlock(&mutx);
}
void error_handling(char * msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> 
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>
	
#define BUF_SIZE 100
#define NAME_SIZE 20
	
void * send_msg(void * arg);
void * recv_msg(void * arg);
void error_handling(char * msg);
	
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];
	
int main(int argc, char *argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
	pthread_t snd_thread, rcv_thread;
	void * thread_return;
	if(argc!=4) {
		printf("Usage : %s <IP> <port> <name>\n", argv[0]);
		exit(1);
	 }
	
	sprintf(name, "[%s]", argv[3]);
	sock=socket(PF_INET, SOCK_STREAM, 0);
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));
	  
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
		error_handling("connect() error");
	
	pthread_create(&snd_thread, NULL, send_msg, (void*)&sock);
	pthread_create(&rcv_thread, NULL, recv_msg, (void*)&sock);
	pthread_join(snd_thread, &thread_return);
	pthread_join(rcv_thread, &thread_return);
	close(sock);  
	return 0;
}
	
void * send_msg(void * arg)   // send thread main
{
	int sock=*((int*)arg);
	char name_msg[NAME_SIZE+BUF_SIZE];
	while(1) 
	{
		fgets(msg, BUF_SIZE, stdin);
		if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")) 
		{
			close(sock);
			exit(0);
		}
		sprintf(name_msg,"%s %s", name, msg);
		write(sock, name_msg, strlen(name_msg));
	}
	return NULL;
}
	
void * recv_msg(void * arg)   // read thread main
{
	int sock=*((int*)arg);
	char name_msg[NAME_SIZE+BUF_SIZE];
	int str_len;
	while(1)
	{
		str_len=read(sock, name_msg, NAME_SIZE+BUF_SIZE-1);
		if(str_len==-1) 
			return (void*)-1;
		name_msg[str_len]=0;
		fputs(name_msg, stdout);
	}
	return NULL;
}
	
void error_handling(char *msg)
{
	fputs(msg, stderr);
	fputc('\n', stderr);
	exit(1);
}

七、简易HTTP服务器端

HTTP

Web服务器:基于 HTTP协议,将网页对应文件传输给客户端的服务器端。

HTTP协议:HTTP 是以超文本传输为目的而设计的应用层协议。

  • 请求消息的结构

  • 响应消息的结构

常见状态码:

  • 200 OK:成功处理了请求!

  • 404 Not Found:请求的文件不存在!

  • 400 Bad Request:请求方式错误,请检查!

基于多线程的Web服务器端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>

#define BUF_SIZE 1024
#define SMALL_BUF 100

void* request_handler(void* arg);
void send_data(FILE* fp, char* ct, char* file_name);
char* content_type(char* file);
void send_error(FILE* fp);
void error_handling(char* message);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	int clnt_adr_size;
	char buf[BUF_SIZE];
	pthread_t t_id;	
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port = htons(atoi(argv[1]));
	if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 20)==-1)
		error_handling("listen() error");

	while(1)
	{
		clnt_adr_size=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size);
		printf("Connection Request : %s:%d\n", 
			inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port));
		pthread_create(&t_id, NULL, request_handler, &clnt_sock);
		pthread_detach(t_id);
	}
	close(serv_sock);
	return 0;
}

void* request_handler(void *arg)
{
	int clnt_sock=*((int*)arg);
	char req_line[SMALL_BUF];
	FILE* clnt_read;
	FILE* clnt_write;
	
	char method[10];
	char ct[15];
	char file_name[30];
  
	clnt_read=fdopen(clnt_sock, "r");
	clnt_write=fdopen(dup(clnt_sock), "w");
	fgets(req_line, SMALL_BUF, clnt_read);	
	if(strstr(req_line, "HTTP/")==NULL)
	{
		send_error(clnt_write);
		fclose(clnt_read);
		fclose(clnt_write);
		return;
	 }
	
	strcpy(method, strtok(req_line, " /"));
	strcpy(file_name, strtok(NULL, " /"));
	strcpy(ct, content_type(file_name));
	if(strcmp(method, "GET")!=0)
	{
		send_error(clnt_write);
		fclose(clnt_read);
		fclose(clnt_write);
		return;
	 }

	fclose(clnt_read);
	send_data(clnt_write, ct, file_name); 
}

void send_data(FILE* fp, char* ct, char* file_name)
{
	char protocol[]="HTTP/1.0 200 OK\r\n";
	char server[]="Server:Linux Web Server \r\n";
	char cnt_len[]="Content-length:2048\r\n";
	char cnt_type[SMALL_BUF];
	char buf[BUF_SIZE];
	FILE* send_file;
	
	sprintf(cnt_type, "Content-type:%s\r\n\r\n", ct);
	send_file=fopen(file_name, "r");
	if(send_file==NULL)
	{
		send_error(fp);
		return;
	}

	/* 传输头信息 */
	fputs(protocol, fp);
	fputs(server, fp);
	fputs(cnt_len, fp);
	fputs(cnt_type, fp);

	/* 传输请求数据 */
	while(fgets(buf, BUF_SIZE, send_file)!=NULL) 
	{
		fputs(buf, fp);
		fflush(fp);
	}
	fflush(fp);
	fclose(fp);
}

char* content_type(char* file)
{
	char extension[SMALL_BUF];
	char file_name[SMALL_BUF];
	strcpy(file_name, file);
	strtok(file_name, ".");
	strcpy(extension, strtok(NULL, "."));
	
	if(!strcmp(extension, "html")||!strcmp(extension, "htm")) 
		return "text/html";
	else
		return "text/plain";
}

void send_error(FILE* fp)
{	
	char protocol[]="HTTP/1.0 400 Bad Request\r\n";
	char server[]="Server:Linux Web Server \r\n";
	char cnt_len[]="Content-length:2048\r\n";
	char cnt_type[]="Content-type:text/html\r\n\r\n";
	char content[]="<html><head><title>NETWORK</title></head>"
	       "<body><font size=+5><br>发生错误!查看请求文件名和请求方式"
		   "</font></body></html>";

	fputs(protocol, fp);
	fputs(server, fp);
	fputs(cnt_len, fp);
	fputs(cnt_type, fp);
	fflush(fp);
}

void error_handling(char* message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

通过虚拟机运行服务器端代码,利用8000号端口提供 HTTP 服务

gcc webserv_linux.c -D_REENTRANT -o wserv -lpthread
./ wserv 8000

在虚拟机的浏览器中访问网址 http://127.0.0.1:8000/index.htmlhttp://localhost:8000/index.html (需要将 html 文件放到服务器端代码同级目录下)

总结

通过《TCP/IP 网络编程》这本书,初步了解了网络编程,对网络编程有了大致的了解,为后续阅读《Linux 高性能服务器编程》并完成webserver 项目打下基础。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值