TCP/IP网络编程C04-基于TCP的服务端⁄客户端(1)

学习笔记

理解TCP和UDP

TCP/IP协议栈共分4层

+--------------------+
|     Application    |
+--------------------+
|       TCP/UDP      |
+--------------------+
|         IP         |
+--------------------+
|      Ethernet      |
+--------------------+

TCP服务器端默认函数调用顺序

+------------------+
|      socket()    |创建套接字
+------------------+
|       bind()     |分配套接字地址
+------------------+
|      listen()    |监听连接
+------------------+
|      aceept()    |允许连接
+------------------+
|  read()/write()  |数据交换
+------------------+
|      close()     |断开连接
+------------------+

TCP客户端默认函数调用顺序

+------------------+
|      socket()    |创建套接字
+------------------+
|      connect()   |请求连接
+------------------+
|  read()/write()  |数据交换
+------------------+
|      close()     |断开连接
+------------------+

实现基于TCP的服务器端/客户端

服务器端进入等待连接请求状态

#include <sys/socket.h>
int listen(int sockfd, int backlog);  
// 功能:将套接字转换为可接收连接的状态。
// 参数:sock:希望进入等待连接请求状态的套接字文件描述符;backlog:连接请求等待队列的最大长度,最多使 backlog 个连接请求进入队列。
// 返回值:成功时返回 0,失败时返回 -1

等待连接请求状态:当服务器在此状态下时,在调用 accept 函数受理连接请求前,请求会处于等待状态。注意:这里说的是让来自客户端的请求处于等待状态,以等待服务器端受理它们的请求。

连接请求等待队列:还未受理的连接请求在此排队,backlog 的大小决定了队列的最大长度,一般频繁接受请求的 Web 服务器的 backlog 至少为 15。

accept()函数不参与三次握手,而只负责从建立连接队列中取出一个连接和socketfd进行绑定

服务器端受理客户端连接请求

#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t addrlen);  
// 功能:受理连接请求等待队列中待处理的连接请求。
// 参数:sock:服务器套接字的文件描述符;addr:用于保存发起连接请求的客户端地址信息;addrlen:第二个参数的长度。
// 返回值:成功时返回创建的套接字文件描述符,失败时返回 -1

accept 函数会受理连接请求等待队列中待处理的客户端连接请求,它从等待队列中取出 1 个连接请求,创建套接字并完成连接请求。如果等待队列为空,accpet 函数会阻塞,直到队列中出现新的连接请求才会返回。

它会在内部产生一个新的套接字并返回其文件描述符,该套接字用于与客户端建立连接并进行数据 I/O。新的套接字是在 accept 函数内部自动创建的,并自动与发起连接请求的客户端建立连接。(调用socket()创建的套接字称为守门员套接字)

accept 执行完毕后会将它所受理的连接请求对应的客户端地址信息存储到第二个参数 addr 中。

客户端发起连接请求

#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);  
// 功能:请求连接。
// 参数:sock:客户端套接字的文件描述符;serv_addr:保存目标服务器端地址信息的结构体指针;addrlen:第二个参数的长度(单位是字节)
// 返回值:成功时返回 0,失败时返回 -1

//connect是阻塞函数

客户端调用 connect 函数后会阻塞,直到发生以下情况之一才会返回:

  1. 服务器端接收连接请求。

  2. 发生断网等异常情况而中断连接请求。

注意:上面说的”接收连接请求“并不是服务器端调用了 accept 函数,而是服务器端把连接请求信息记录到等待队列。因此 connect 函数返回后并不立即进行数据交换。(connect 函数的作用是和服务器端完成三次握手)

基于TCP的服务器端/客户端函数调用关系


客户端只有等到服务器端调用 listen 函数后才能调用 connect 函数,否则会连接失败。
客户端调用 connect 函数和服务器端调用 accept 函数的顺序不确定,先调用的要等待另一方。

实现迭代服务器端/客户端

回声服务器端:它会将客户端传输的字符串数据原封不动地传回客户端,像回声一样。

实现迭代服务器端

调用一次 accept 函数只会受理一个连接请求,如果想要继续受理请求,最简单的方法就是循环反复调用 accept 函数,在前一个连接 close 之后,重新 accept。

在不使用多进程/多线程情况下,同一时间只能服务于一个客户端。

迭代回声服务器端/客户端

迭代回声服务器端与回声客户端的基本运行方式:

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

  2. 服务器端依次向 5 个客户端提供服务,然后退出。

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

  4. 服务器端将接收到的字符串数据传回客户端,即”回声“。

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

习题答案

Q01

  1. 请说明 TCP/IP 的 4 层协议栈,并说明 TCP 和 UDP 套接字经过的层级结构差异

    差异是一个经过 TCP 层,一个经过 UDP 层。

Q02

  1. 请说出 TCP/IP 协议栈中链路层和 IP 层的作用,并给出两者关系。

    链路层是 LAN、WAN、MAN 等网络标准相关的协议栈,是定义物理性质标准的层级。相反,IP 层是定义网络传输数据标准的层级。即 IP 层负责以链路层为基础的数据传输

    链路层是物理链接领域标准化的结果,专门定义网络标准。若两台主机通过网络进行数据交换,则首先要做到的就是进行物理链接。IP层:为了在复杂的网络中传输数据,首先需要考虑路径的选择。关系:链路层负责进行一系列物理连接,而IP层负责选择正确可行的物理路径。

Q03

  1. 为何需要把 TCP/IP 协议栈分成 4 层(或 7 层)?结合开放式系统回答

    将复杂的 TCP/IP 协议分层化的话,就可以将分层的层级标准发展成开放系统。实际上,TCP/IP 是开放系统,各层级都被初始化,并以该标准为依据组成了互联网。因此,按照不同层级标准,硬件和软件可以相互替代,这种标准化是 TCP/IP 蓬勃发张的依据

    ARPANET 的研制经验表明,对于复杂的计算机网络协议,其结构应该是层次式的。分层的好处:①隔层之间是独立的②灵活性好③结构上可以分隔开④易于实现和维护⑤能促进标准化工作。

Q04

  1. 客户端调用 connect 函数向服务器端发送连接请求。服务器端调用哪个函数后,客户端可以调用 connect 函数?

    服务端调用 listen 函数后,客户端可以调用 connect 函数。因为,服务端调用 listen 函数后,服务端套接字才有能力接受请求连接的信号。

Q05

  1. 什么时候创建连接请求等待队列?它有何作用?与 accept 有什么关系

    服务端调用 listen 函数后,accept函数正在处理客户端请求时, 更多的客户端发来了请求连接的数据,此时,就需要创建连接请求等待队列。以便于在accept函数处理完手头的请求之后,按照正确的顺序处理后面正在排队的其他请求。与accept函数的关系:accept函数受理连接请求等待队列中待处理的客户端连接请求。

Q06

  1. 客户端中为何不需要调用 bind 函数分配地址?如果不调用 bind 函数,那何时、如何向套接字分配 IP 地址和端口号?

    在调用 connect 函数时分配了地址,客户端IP地址和端口在调用 connect 函数时自动分配,无需调用标记的 bind 函数进行分配。

Q07

  1. 把第1章的hello_server. c和 hello_server win.c改成迭代服务器端,并利用客户端测试更改是否准确。

改成迭代服务器端(这里迭代的意思是指服务完一个立马服务下一个,持续不断地服务)

#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);  
	
	while(1)//while循环,不断迭代
	{
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
		if(clnt_sock==-1)
			break;  
		
		write(clnt_sock, message, sizeof(message));
		close(clnt_sock);
	}
	close(serv_sock);
	return 0;
}

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


书本源码

01-echo_server.c



#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");
	
	//设置服务端IP和端口
	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);
		/*
		如果服务端进程关闭了socket连接,那么客户端会接收到服务端发送过来的一个 TCP 协议的 FIN 数据包,然后客户端进程中原本阻塞着等待接收服务端进程数据的 read函数此时就会被唤醒,返回一个值 0。
		这跟我们前面提到两种文件读到文件末尾返回 EOF(值为-1)的情况有点差别,所以在程序中从 socket 进行读取操作时,判断数据流结束的标志不是 -1 而是 0。
		*/	
		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);
}


/******************** input******************
description:



content:

./01-echo_server 9190

*******************************************/



/******************** output******************
description:
服务器端在同一时刻只与一个客户端相连,并提供回声服务


content:
Connected client 1 
Connected client 2 
Connected client 3 

*******************************************/



02-echo_client.c



#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");
	

	//设置目的端的IP和端口
	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;

		write(sock, message, strlen(message));//函数strlen()所计算的长度不包括`\0`

		/*
		在本章的回声客户端的实现中有上面这段代码,它有一个错误假设:每次调用 read、write 函数时都会执行实际的 I/O 操作。
		但是注意:TCP 是面向连接的字节流传输,不存在数据边界。所以多次 write 的内容可能一直存放在发送缓存中,某个时刻再一次性全都传递到服务器端,这样的话客户端前几次 read 都不会读取到内容,最后才会一次性收到前面多次 write 的内容。还有一种情况是服务器端收到的数据太大,只能将其分成多个数据包发送给客户端,然后客户端可能在尚未收到全部数据包时旧调用 read 函数。
		理解:问题的核心在于 write 函数实际上是把数据写到了发送缓存中,而 read 函数是从接收缓存读取数据。并不是直接对 TCP 连接的另一方进行数据读写。

		*/
		str_len=read(sock, message, BUF_SIZE-1);//这段代码有个错误假设

		//'\0'是一个“空字符”常量,它表示一个字符串的结束,它的ASCII码值为0
		//'0'的ASCII码值为48(十进制)
		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);
}

/******************** input******************
description:



content:
./02-echo_client  127.0.0.1 9190
./02-echo_client  127.0.0.1 9190
./02-echo_client  127.0.0.1 9190


*******************************************/



/******************** output******************
description:



content:

Connected...........
Input message(Q to quit): 213
Message from server: 213
Input message(Q to quit): q

*******************************************/



参考链接

posted @ 2021-12-17 10:58  MyBluehat  阅读(68)  评论(0)    收藏  举报