【TCP/IP】基于TCP的服务器端/客户端 I - 原理及初步实现(C++)

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

1.1 进入等待请求状态

1.2 受理客户端连接请求

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

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


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

        TCP服务器端多按照以下顺序进行函数调用:

1.1 进入等待请求状态

        当调用完bind函数给套接字分配了地址,接下来就要通过调用listen函数进入等待连接请求状态。只有调用listen函数才能使客户端进入到可发出连接请求的状态,从而使客户端能够调用connect函数。

#include <sys/socket.h>

int listen(int sock , int backlog);

//成功时返回0,失败时返回-1

//sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)。
//backlog:连接请求等待队列(Queue)的长度,表示有n个连接请求进入队列。

        “服务器端处于等待连接请求状态”一般是指在客户端请求连接(服务器端)时,受理连接前一直使请求处于等待状态。

1.2 受理客户端连接请求

        调用listen函数后,若有新的连接请求,则应按序受理。下面的函数能够帮助自动创建套接字,并连接到发起请求的客户端。

#include <sys/socket.h>

int accept(int sock , struct sockaddr * addr , socklen_t * addrlen);

//成功时返回创建的套接字文件描述符,失败时返回-1

//sock:服务器套接字的文件描述符
//addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息。
//addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完成后,该变量即被填入客户端地址长度。

        accept函数用来受理等待队列中待处理的客户端连接请求。函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符。需强调的是,套接字是自动创建的,并自动与发起连接请求的客户端建立连接。

代码回顾(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(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);
}
  • 第21行:服务器端实现过程中先要创建套接字。第21行创建套接字,但此时的套接字尚非真正的服务器端套接字。
  • 第25-31行:为了完成套接字地址分配,初始化结构体变量并调用 bind 函数。
  • 第33行:调用 listen 函数进入等待连接请求状态。连接请求等待队列的长度设置为5。此时的套接字才是服务器端套接字。
  • 第37行:accept 函数从队头取 1 个连接请求与客户端建立连接,并返回创建的文件描述符。另外,调用 accept 函数时若等待队列为空,则accept 函数不会返回,直到队列中出现新的客户端连接。
  • 第41、42行:调用 write 函数向客户端传输数据,调用 close 函数关闭连接。

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

        相对于服务器端,客户端的调用流程相对简单。创建套接字并请求连接基本上就是客户端的全部内容,其流程如下:

        与服务器端相比,客户端的设计重点在于“请求连接”,即在创建客户端套接字后向服务器端发起的连接请求。服务器端调用listen函数后创建连接请求等待队列,之后客户端即可请求连接。在操作上我们常通过connect函数来完成该操作。

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

        客户端调用connect函数后,发生以下情况之一便会返回(完成函数调用)

  • 服务器端接收连接请求
  • 发生断网等异常情况而终端连接请求 

        “接收连接”并不意味着服务器端调用accept函数,而是服务器端把连接请求信息记录到等待队列。因此connect函数返回后并不立即进行数据交换。

代码回顾(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 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");
	
	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);
}
  • 第17行:创建准备连接服务器端的套接字,此时创建的是TCP套接字。
  • 第21~24行:结构体变量serv_addr 中初始化IP和端口信息。初始化值为目标服务器端套接字的IP和端口信息。
  • 第26行:调用 connect 函数向服务器端发送连接请求。
  • 第29行:完成连接后,接收服务器端传输的数据。
  • 第34行:接收数据后调用 close 函数关闭套接字,结束与服务器端的连接。

补充:客户端套接字地址信息在哪?

服务器端的通信实现必经过程之一就是给套接字分配IP和端口号。但客户端实现过程中并没有进行套接字地址分配,而是创建套接字后调用connect函数。那么这样来说,客户端套接字是无需分配IP和端口的吗?并不是!网络数据交换必须分配IP和端口。那客户端套接字是在何时、何地以及如何分配地址的呢?

  • 何时:调用connect函数时
  • 何地:OS,OS的内核中
  • 分配规则: IP使用主机IP地址,端口随机

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

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

        TCP服务器端/客户端两者间并非相互独立,他们的总体流程可以概述为:服务器端创建套接字后连续调用bind,listen函数并进入等待状态,客户端通过调用connect函数发起连接请求。客户端智能等到服务器端调用listen函数后才能调用connect函数。需要明确,客户端在调用connect函数前,服务器端可能会先去调用accept函数。所以,在客户端调用connect函数期间服务器端会进入到阻塞状态,直到客户端调用完connect函数时再解除阻塞,调用accept。

        两者交互过程如下图所示:

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

干吃咖啡豆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值