TCP服务器客户端编程流程

TCP服务器客户端编程流程

  • TCP编程流程
  • 主机字节序列和网络字节序列
  • 套接字地址结构
    • 通用socket地址结构
    • 一般使用会定义一个专用的套接字结构
  • IP地址转换函数
  • 网络编程接口
  • TCP服务段代码实现
  • TCP客户端代码实现
  • TCP服务端客户端代码详解
  • TCP流式服务和粘包问题
  • netstat命令

TCP编程流程

TCP提供的是面向连接、可靠的、字节流服务
TCP服务器客户端编程流程_第1张图片
客户端与服务端分别在两个不同的主机上,TCP连接流程固定

首先第一步是创建套接字socket()(客户端创建套接字一般不需要指定端口号),相当于区分不同的连接,然后绑定bind()来指定ip与端口,然后创建监听队列listen(),等待接收连接accept()会阻塞在这一步等待链接,这个时候客户端执行connect()建立链接然后会进行三次握手进行连接,随后客户端与服务端就可以发送接收数据了,close()关闭套接字

主机字节序列和网络字节序列

主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。 在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。

不同主机对字节的存储顺序可能不一样,存在小段字节序和大段字节序

Linux系统提供了如下4个函数来完成主机字节序和网络字节序之间的转换:
TCP服务器客户端编程流程_第2张图片
较常用的:uint16_t htons(uint16_t hostshort) 主机序列转网络序列,十六位较多

套接字地址结构

通用socket地址结构

socket网络编程接口中表示socket地址的是结构体sockaddr(对链接到唯一表示),其定义如下:
TCP服务器客户端编程流程_第3张图片
这是通用套接字结构

一般使用会定义一个专用的套接字结构

TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分
别用于 IPV4 和 IPV6:
TCP服务器客户端编程流程_第4张图片
TCP服务器客户端编程流程_第5张图片
TCP服务器客户端编程流程_第6张图片

IP地址转换函数

通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换:
在这里插入图片描述
将字符串,转为32位整型
TCP服务器客户端编程流程_第7张图片

网络编程接口

  • int socket(int domain, int type, int protocol);
    创建套接字,成功返回套接字文件描述符;参数:协议族,服务类型,最后一般设0,表示默认协议
  • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    绑定套接字,成功返回0;参数:套接字描述符,地址结构,socket地址长度
  • int listen(int sockfd, int backlog);
    创建一个监听队列以存储待处理的客户连接,成功返回0;参数:被监听的socket套接字,处于完全连接状态的socket上限
  • int accept(int sockfd, struct sockaddr *addr,socklen_t *addrlen);
    accept()从listen监听队列中接收一个连接,成功返回一个新的连接socket,该socket唯一标识了被接受的这个连接;参数:监听socket,获取被接受连接的远端socket地址,指定socket地址长度
  • int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
    connect()客户端需要通过此系统调用来主动与服务器建立连接,成功返回0;参数:socket()返回的参数,服务器监听的socket地址,地址长度
  • int close(int sockfd);
    close()关闭一个连接,就是关闭该连接对应的socket套接字
  • ssize_t recv(int sockfd, void *buff, size_t len, int flags);
  • ssize_t send(int sockfd, const void *buff, size_t len, int flags);
    recv()读取套接字是的数据,buff与len参数分别指顶读取缓冲区的位置和大小
    send()发送套接字是的数据,buff与len参数分别指顶写缓冲区的位置和数据长

TCP服务段代码实现

#include
#include
#include
#include
#include
#include
#include
#include

int main()
{
     
	//创建套接字
	//参数:
	//AF_INET 指定协议族为ipv4
	//SOCK_STREAM 服务类型 使用流式套接字
	//一般为0
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	//创建套接字地址结构
	struct sockaddr_in saddr,caddr;//caddr存放客户端的ip与端口
	memset(&saddr,0,sizeof(saddr));//清空saddr
	saddr.sin_family = AF_INET;//协议族或者叫地址族
	saddr.sin_port = htons(6000);//端口 转换主机序列到网络序列
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip 字符串转换为整形,127.0.0.1是用于本地测试的ip地址

	//绑定套接字
	//参数:
	//套接字描述符
	//地址结构,需要传入通用套接字结构所有进行强转
	//地址结构大小
	int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	assert(res != -1);
	
	//创建监听队列
	//参数:
	//套接字描述符
	//监听队列长度
	listen(sockfd,5);

	while(1)
	{
     
		//循环 接受客户端的连接
		//参数:
		//套接字描述符
		//存放客户端的地址结构
		//地址结构大小
		int len = sizeof(saddr);
		int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
		if(c < 0)
		{
     
			continue;
		}

		printf("accept c = %d\n",c);

		char buff[128] = {
     0};
		recv(c,buff,127,0);
		//从c 也就是客户端接受数据,存放在buff,大小为127,防止将其占满,标志位给0
		//也可以通过read操作,因为c也是文件描述符
		//实际上是与c进行沟通,而sockfd只作用于循环前
		printf("buff = %s\n",buff);
		send(c,"ok",2,0);
		//发送给c客户端,发送OK,大小为2,标志位0
		//也可以用write(),与上同理

		close(c);//关闭c
		
	}
}

TCP客户端代码实现

#include
#include
#include
#include
#include
#include
#include
#include

int main()
{
     
	//socket() connect() send() recv() close()
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in saddr;//客户端(caddr)自己的端口ip由系统分配
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("127.0.0.1");

	int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	assert(res != -1);

	printf("input:\n");
	char buff[128] = {
     0};
	fgets(buff,127,stdin);
	send(sockfd,buff,strlen(buff),0);//write()也可以做到
	memset(buff,0,128);//清空为了接受 来自服务渠道数据
	recv(sockfd,buff,127,0);//read()也可以做到
	printf("read:%s\n",buff);

	close(sockfd);

}

TCP服务端客户端代码详解

我们将服务器与客户端代码都运行起来查看
在这里插入图片描述
在这里插入图片描述
服务端在这里是死循环一直执行的,只能手动暂停

当我们运行了服务端之后,6000号端口就会被我们占用,我们输入 netstat -natp 指令查看
TCP服务器客户端编程流程_第8张图片
我们看到 “127.0.0.1:6000” 的信息,当我们再次执行服务端代码,就会失败,因为端口已经被占用了
在这里插入图片描述
只有端口被释放出来,才能再次执行

在这里插入图片描述
这里的消息队列长度,可以理解为有座位,座位的数目就是我们的长度,当我们listen后会创建两个队列,一个是未完成三次握手的,一个是完成三次握手的;当我们客户端发起链接后(connect)就会将链接放在上面未完成三次握手的队列中,等完成三次握手就放入下面的队列中,而accept就相当于从完成三次握手的队列中拿到链接进行处理得到一个新的c,就可以通过c进行交互,完成三次握手的队列长度就是我们设置的5,这里之所以设置长度,就是为了防止等待造成的问题,并且这里的5并不代表服务端只能处理5个客户端
TCP服务器客户端编程流程_第9张图片
listen并不会阻塞,而accept会阻塞,例如我们在此处代码加一行打印,我们编译运行服务器后会发现,服务器代码会阻塞在这一步
在这里插入图片描述在这里插入图片描述
等待客户端的连接,这时候运行客户段代码
在这里插入图片描述
就会继续运行,阻塞在recv处等待接收数据,而客户端阻塞在fgets处,客户端输入数据后服务端继续运行
TCP服务器客户端编程流程_第10张图片
这时候客户端运行结束,服务器继续阻塞等待新的客户段连接,我们再次运行客户端代码,服务端就会接着下一步
TCP服务器客户端编程流程_第11张图片
服务段的ip与端口,在写客户端代码的时候需要自己添加进去,而客户端的ip与端口则是由系统自动生成的,其他更多的在代码注释中介绍

TCP流式服务和粘包问题

TCP服务器客户端编程流程_第12张图片
当我们通过TCP发送数据,并不是直接发送给对方,而是先将数据存放在发送缓冲区中,形成字节流;然后按照底层协议将数据重新打包再发送给对方,对方得到打包的数据,再将其放在接收TCP缓冲区,再接收到的数据就是一整个结合
TCP服务器客户端编程流程_第13张图片
这样看就会出现我们说的粘包问题(多次发送的数据会被对方一次收到),粘包问题有时候会有影响有时候没有影响,比如我们传输一个文件,至于数据发送几次接受几次并不影响文件传输的目的,而假如我们去发送长方体的长宽高,需要接收方返回体积,这时候我们send()发送了三次,发生粘包问题,就会只收到一次就不能区分长宽高
我们可以通过一个方法来解决这类问题,再send()之后,加一个recv()来接收对方接收成功发来的讯息,这样就可以将不同的数据进行错开;或者可以通过将发送数据按某种格式进行分割(【3】【4】【5】,例如这样通过中括号进行分割)
TCP服务器客户端编程流程_第14张图片
我们在执行send()的时候只能说明数据放在了发送缓冲区中,但是是否发送给了对方的接收缓冲区我们并不知道
TCP服务器客户端编程流程_第15张图片

netstat命令

TCP服务器客户端编程流程_第16张图片
在这里插入图片描述

我们将原先服务端代码中的循环部分进行修改如下:

while(1)
	{
     
		int len = sizeof(saddr);
		printf("accept wait ......\n");
		int c = accept(sockfd,(struct sockaddr*)&caddr,&len);
		if(c < 0)
		{
     
			continue;
		}

		printf("accept c = %d\n",c);
		while(1)
		{
     
			char buff[128] = {
     0};
			int n = recv(c,buff,127,0);
			if(n<=0)
			{
     
				break;
			}
			printf("buff = %s\n",buff);
			send(c,"ok",2,0);
		}

		close(c);
		
	}

原本只进行一次接收并打印,随后就会关闭链接,现在我们将其循环起来,只要对方不关闭链接(recv返回值为0)就能不断接收数据

我们将原本的客户端代码也进行修改

while(1)
	{
     
		printf("input:\n");
		char buff[128] = {
     0};
		fgets(buff,127,stdin);
		if(strncmp(buff,"end",3) == 0)
		{
     
			break;
		}
		send(sockfd,buff,strlen(buff),0);//write()也可以做到
		memset(buff,0,128);//清空为了接受 来自服务渠道数据
		recv(sockfd,buff,127,0);//read()也可以做到
		printf("read:%s\n",buff);
	}
	close(sockfd);

将原本只发送并接受一次数据就关闭链接,修改为只要不从键盘获取end,就不断发送获取数据

我们将代码编译运行起来
TCP服务器客户端编程流程_第17张图片
我们再次修改服务端的代码,我们将此处本来只能获得127字节大小改为1,每次接收1字节并打印
TCP服务器客户端编程流程_第18张图片
我们再次运行代码查看
TCP服务器客户端编程流程_第19张图片
有些主机可能会出现,直接受到一个ok的返回
我们使用netstat -natp来查看
TCP服务器客户端编程流程_第20张图片
可以看到客户端与服务端的端口与ip,再去观察我们的接收缓冲区,假如向下下图那样缓冲区并不为空,说明还有数据没有被recv接收(Recv-Q 接收缓冲区 Send-Q 发送缓冲区)
TCP服务器客户端编程流程_第21张图片
TCP服务器客户端编程流程_第22张图片

你可能感兴趣的:(tcp/ip,服务器,网络,c语言,linux)