基于Linux用C语言实现TCP半双工通信和UDP半双工通信

移步我的个人博客阅读体验可能会更佳哦!欢迎小伙伴访问我的个人博客
https://topking66.gitee.io
本文的博客链接:
https://topking66.gitee.io/posts/9589.html

文章目录

    • TCP协议/UDP协议介绍
    • 三种通信方式
    • 实现TCP半双工通信
      • 所用到的结构体与函数
      • 源代码
      • 运行结果
    • 实现UDP半双工通信
      • 源代码
      • 运行结果
    • 参考文章

TCP协议/UDP协议介绍

  • TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇, 只是因为在TCP/IP协议中TCP协议和IP协议最具代表性,所以被称为TCP/IP协议
  • 下面我们来简要了解TCP/IP的四层模型:
    网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。数据帧是网络信息传输的基本单元,ARP和RARP协议
    网络层:负责将数据帧封装成IP数据报,同时负责选择数据报的路径,即路由,IP和ICMP协议
    传输层:负责端到端之间的通信会话连接与建立,传输协议的选择根据数据传输方式而定,UDP和TCP协议
    应用层:负责应用程序的网络访问,这里通过端口号来识别各个不同的进程,FTP、TELNET、DNS、SMTP、POP3 协议

基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第1张图片

  • Internet 的传输层有两个主要协议,互为补充。无连接的是 UDP,它除了给应用程序发送数据包功能并允许它们在所需的层次上架构自己的协议之外,几乎没有做什么特别的事情。面向连接的是 TCP,该协议几乎做了所有的事情。

  • 传输控制协议(TCP,Transmission Control Protocol) 是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后,等待对方回答SYN+ACK,并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP使用的流量控制协议是可变大小的滑动窗口协议。

    TCP三次握手过程
    第一次握手:主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B,向主机B 请求建立连接,通过这个数据段, 主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。
    第二次握手:主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用那个序列号作为起始数据段来回应我
    第三次握手:主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了,这样3次握手就完成了,主机A和主机B 就可以传输数据了。

    TCP建立连接要进行3次握手,而断开连接要进行4次
    第一次: 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求 ;
    第二次: 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1;
    第三次: 由B 端再提出反方向的关闭请求,将FIN置1 ;
    第四次: 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.。
    由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式, 大大提高了数据通信的可靠性,使发送数据端和接收端在数据正式传输前就有了交互, 为数据正式传输打下了可靠的基础。

  • UDP(User Datagram Protocol) 全称是用户数据报协议,是一种非面向连接的协议,这种协议并不能保证我们的网络程序的连接是可靠的,而TCP是面向连接的,提供可靠的字节流。然而,有些情况下更适合用UDP而不是TCP。有些流行的应用程序是用UDP实现的:DNS(域名系统)、NFS(网络文件系统)和SNMP(简单网络管理协议)就是这样的例子。
    1.UDP是一个非连接的协议,传输数据之前源端和终端不建立连接, 当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、 计算机的能力和传输带宽的限制; 在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。

    2.由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态等, 因此一台服务机可同时向多个客户机传输相同的消息。

    3.UDP信息包的标题很短,只有8个字节,相对于TCP的20个字节信息包的额外开销很小。

    4.吞吐量不受拥挤控制算法的调节,只受应用软件生成数据的速率、传输带宽、 源端和终端主机性能的限制。

    5.UDP使用尽最大努力交付,即不保证可靠交付, 因此主机不需要维持复杂的链接状态表(这里面有许多参数)。

    6.UDP是面向报文的。发送方的UDP对应用程序交下来的报文, 在添加首部后就向下交付给IP层。既不拆分,也不合并,而是保留这些报文的边界, 因此,应用程序需要选择合适的报文大小。

    我们经常使用“ping”命令来测试两台主机之间TCP/IP通信是否正常, 其实“ping”命令的原理就是向对方主机发送UDP数据包,然后对方主机确认收到数据包, 如果数据包是否到达的消息及时反馈回来,那么网络就是通的。

  • 小结TCP与UDP的区别:

    1、基于连接与无连接;

    2、对系统资源的要求(TCP较多,UDP少);

    3、UDP程序结构较简单;

    4、流模式与数据报模式 ;

    5、TCP保证数据正确性,UDP可能丢包;

    6、TCP保证数据顺序,UDP不保证。 ## 三种通信方式


三种通信方式

基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第2张图片

1、单向通信:又称为单工通信,即只能有一个方向的通信而没有反方向的交互。无线电广播或有线电广播以及电视广播就属于这种类型。
单向通信只需要一条信道,而双向交替通信或双向同时通信则都需要两条信道(每个方向各一条)。显然,双向同时通信的传输效率最高。不过应当指出,虽然电信局为打电话的用户提供了双向同时通信的信道,但有效的电话交谈一般都还是双方交替通信。当双方发生争吵时往往就是采用双向同时通信的方式。

2、半双工通信,是指数据可以沿两个方向传送.但同一时刻一个信道只允许单方向传送,因此义被称为双向交替通信。例如,无线对讲机就是一种半双工设备,在同一时间内只允许一方讲话。

3、全双工通信,是指同时发生在两个方向上的一种数据传输方式,如图中©所示。电话机就是一种全双工设备,其通话双方可以同时进行对话。计算机之间的高速数据通信也是这种方式。

双向交替通信又称为半双工通信,即通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收)。这种通信方式是一方发送另一方接收,过一段时间后再反过来。此次要实现的就是半双工通信


实现TCP半双工通信

基于TCP协议的socket的server端程序编程步骤:
1、建立socket ,使用socket()
2、绑定socket ,使用bind()
3、打开listening socket,使用listen()
4、等待client连接请求,使用accept()
5、收到连接请求,确定连接成功后,使用输入,输出函数recv(),send()与client端互传信息
6、关闭socket,使用close()

基于TCP协议的socket的Client程序编程步骤:
1、建立socket,使用socket()
2、通知server请求连接,使用connect()
3、若连接成功,就使用输入输出函数recv(),send()与server互传信息
4、关闭socket,使用close()

所用到的结构体与函数

1.IPV4套接字地址结构体:

struct sockaddr_in{
    uint8_t             sin_len;
    sa_famliy_t         sin_fanliy;/*协议家族*/
    in_port_t           sin_port;/*端口号*/
    struct in_addr      sin_addr;/*IP地址,struct in_addr{in_addr_t s_addr;}*/
    char                sin_zero[8];
};

2.通用套接字地址结构体:

struct sockaddr{
    uint8_t       sa_len;
    sa_famliy     sa_famliy;
    char          sa_data[14];
};

3.socket():

int socket(int domain,int type, int protocol);
/*
创建一个套接字:
返回值:
    创建成功返回一个文件描述符(0,1,2已被stdin、stdout、stderr占用,所以从3开始)
    失败返回-1。
参数:
    domain为协议家族,TCP属于AF_INET(IPV4);
    type为协议类型,TCP属于SOCK_STREAM(流式套接字);
    最后一个参数为具体的协议(IPPOOTO_TCP为TCP协议,前两个已经能确定该参数是TCP,所以也可以填0)
*/

4.bind():

int bind(int sockfd,const struct sockaddr * addr,socklen_t addrlen);
/*
将创建的套接字与地址端口等绑定
返回值:成功返回0,失败返回-1.
参数:
    sockfd为socket函数返回接受的文件描述符,
    addr为新建的IPV4套接字结构体
    注意:定义若是使用struct sockaddr_in(IPV4结构体)定义,但是该参数需要将struct sockaddr_in *类型地址强转为struct sockaddr *类型(struct sockaddr是通用类型)。
    最后一个参数为该结构体所占字节数。
*/

5.listen():

int listen(int sockfd,int backlog);
/*
对创建的套接字进行监听,监听有无客户请求连接
返回值:有客户请求连接时,返回从已完成连接的队列中第一个连接(即完成了TCP三次握手的的所有连接组成的队列),否则处于阻塞状态(blocking)。
参数:
sockfd依然为socket函数返回的文件描述符;
blocklog为设定的监听队列的长度。可设为5、10等值但是不能大于SOMAXCONN(监听队列最大长度)
*/

6.connect()和accept():

int connect(int sockfd,const struct sockaddr * addr,socklen_t addrlen);
/*
客户端请求连接
返回值:成功返回0,失败返回-1
参数:客户端的socket文件描述符,客户端的socket结构体地址以及结构体变量长度
*/
int accept(int sockfd,struct sockaddr * addr,socklen_t * addrlen);
/*
从监听队列中接收已完成连接的第一个连接
返回值:成功返回0,失败返回-1
参数:服务器socket未见描述符,服务器的socket结构体地址以及结构体变量长度
*/

7.send()和recv():

ssize_t send(int sockfd,const void * buf,size_t len,int flags);
/*
发送数据
返回值:成功返回发送的字符数,失败返回-1
参数:buf为写缓冲区(send_buf),len为发送缓冲区的大小,flags为一个标志,如MSG_OOB表示有紧急带外数据等
*/
ssize_t recv(int sockfd,void *buf, size_t len, int flags);
/*
接收数据
返回值参数与send函数相似
不过send是将buf中的数据向外发送,而recv是将接收到的数据写到buf缓冲区中。
*/

8.close():

int close(int fd);
/*
关闭套接字,类似于fclose,fd为要关闭的套接字文件描述符
失败返回-1,成功返回0
*/

源代码

服务端:

tcp_server.c

#include 
#include 
#include 
#include 
#include 

int main( int argc, char **argv ) /*接收IP地址和端口号*/
{
	int sid = socket(AF_INET,SOCK_STREAM,0);
	//创建socket,第一个参数使用IPv4协议,第二个参数使用流的方式传播,第三个参数默认为0
	struct sockaddr_in addr = {0};  		//定义ip地址
	addr.sin_family = AF_INET;      		//声明协议用的是IPv4
	addr.sin_port = htons(atoi(argv[2])); 	//绑定到哪一个端口号当中,从第三个参数中获取
	
	addr.sin_addr.s_addr = inet_addr(argv[1]); //监听哪一个ip地址,从第二个参数中获取
	int res = bind(sid,(struct sockaddr *)&addr,sizeof(struct sockaddr));//进行绑定,第二个参数为绑定的地址和端口号
	printf("bind res = %d\n",res);	//绑定成功输出0,失败则输出-1
	listen(sid,10);
	//进行监听,第一个参数是监听哪一个socket,第二个是能接受的最大的连接数
	char read_buf[128] = {0};  //储存客户端发来的信息
	char write_buf[128] = {0}; //储存服务端发送的信息
	int len,cid;
	cid = accept(sid,NULL,NULL);//返回客户端的描述符,通过cid可以跟客户端通信
	while(1)
	{
		bzero(read_buf,sizeof(read_buf)); //置字节字符串的前n个字节为零,达到清空之前的信息的目的
		bzero(read_buf,sizeof(write_buf));
		read(cid,read_buf,sizeof(read_buf)-1);
		if(strcmp(read_buf,"quit\n") == 0) //如果收到的信息为quit便结束通信
		{
		   printf("client quit the communication!\n");
		   break;
		}
		   printf("client:%s",read_buf); //输出客户端发送的信息
		   printf("server:");			
		fgets(write_buf,sizeof(write_buf),stdin);
		write(cid,write_buf,strlen(write_buf));	
		if(strcmp(write_buf,"quit\n") == 0) //发出信息quit,结束本次通信
		{
		   printf("sever quit the communication!\n");
		   break;
		}
		
	}
	close(cid);
	close(sid);	
}

客户端:

tcp_client.c

#include 
#include 
#include 
#include 
#include 

int main(int argc,char **argv) /*接收IP地址和端口号*/
{
	if(argc < 3)		//参数必须为三个
	{
		perror("param must >= 3");
		return -1;
	}

	int cid = socket(AF_INET,SOCK_STREAM,0); 
	struct sockaddr_in saddr = {0};//存放要连接的服务器的ip地址和端口号
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(atoi(argv[2]));
	saddr.sin_addr.s_addr = inet_addr(argv[1]);

	int res = connect(cid,(struct sockaddr *)&saddr,sizeof(struct sockaddr)); //跟服务器连接,成功返回0,失败返回-1
	printf("connect res = %d\n",res);

	char read_buf[128] = {0};	//储存客户端发来的信息
	char write_buf[128] = {0};	//储存服务端发送的信息
	while(1)
	{
		bzero(read_buf,sizeof(read_buf));   //置字节字符串的前n个字节为零,达到清空之前的信息的目的
		bzero(write_buf,sizeof(write_buf)); 
		printf("client:");
		fgets(write_buf,sizeof(write_buf),stdin);
		send(cid,write_buf,strlen(write_buf),0);
		if(strcmp(write_buf,"quit\n") == 0) //如果发送的信息为quit便结束本次通信  
		{
		   printf("client quit the communication!\n");
		   break;
		}
		recv(cid,read_buf,sizeof(read_buf)-1,0);
		if(strcmp(read_buf,"quit\n") == 0) //如果收到的信息为quit便结束通信
		{
		   printf("sever quit the communication!\n");
		   break;
		}
		printf("server:%s",read_buf);	//输出客户端发送的信息
	}
	close(cid);
}

运行结果

127.0.0.1是回送地址,指本地机,一般用来测试使用。回送地址(127.x.x.x)是本机回送地址(Loopback Address),即主机IP堆栈内部的IP地址,主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回,不进行任何网络传输。

在Windows系统中,这个地址有一个别名“Localhost”。寻址这样一个地址,是不能发到网络接口的。除非出错,否则在传输介质上永远不应该出现目的地址为“127.0.0.1”的数据包。

运行程序需要打开Linux中打开两个终端,分别编译运行服务端和客户端的源文件,IP地址选择本地地址,端口号建议选择8000以上比较稳定比如9999,首先客户端发送信息到服务端,服务端在接收到客户端的信息后才可以回复,双方都可以输出quit来结束本次通信

客户端:

基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第3张图片

服务端:
基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第4张图片

实现UDP半双工通信

基于UDP协议的socket的接收端编程步骤:
1、建立socket,使用socket()
2、绑定socket,使用bind()
3、以recvfrom()函数接收发送端传来的数据
4、关闭socket,使用close()

基于UDP协议的socket的发送端编程步骤:
1、建立Socket,使socket()
2、用sendto()函数向接收端发送数据。
3、关闭socket,使用close()函数

源代码

接收端:

udp_recvpp.c

#include 
#include 
#include 
#include 
#include 

int main(int argc,char **argv)
{
	if(argc < 3)
	{
		perror("param must >= 3");
		return -1;
	}

	int cid = socket(AF_INET,SOCK_DGRAM,0);
	if(cid < 0)
	{
		perror("socket error");
		return -1;
	}

	struct sockaddr_in addr = {0},sendaddr = {0};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	addr.sin_addr.s_addr = inet_addr(argv[1]);

	int res = bind(cid,(struct sockaddr *)&addr,sizeof(struct sockaddr));
	printf("bind res = %d\n",res);

	char read_buf[128] = {0};
	char write_buf[128] = {0};
	int size = sizeof(struct sockaddr);

	while(1)
	{
		bzero(read_buf,sizeof(read_buf));
		bzero(write_buf,sizeof(write_buf));
		recvfrom(cid,read_buf,sizeof(read_buf) - 1,0,(struct sockaddr *)&sendaddr,&size);
		if(strcmp(read_buf,"quit\n") == 0)
		{
		   printf("send quit the communication!\n");
		   break;
		}
		char ipbuf[16] = {0};
		inet_ntop(AF_INET,&sendaddr.sin_addr.s_addr,ipbuf,sizeof(ipbuf));
		printf("send:%s",read_buf);
		printf("send addr: ip = %s,port = %d\n",ipbuf,ntohs(sendaddr.sin_port));
		 printf("recv:");
		fgets(write_buf,sizeof(write_buf),stdin);
		sendto(cid,write_buf,strlen(write_buf),0,(struct sockaddr *)&sendaddr,sizeof(struct sockaddr));
		if(strcmp(write_buf,"quit\n") == 0)
		{
		   printf("you quit the communication!\n");
		   break;
		}
		
	}
	close(cid);
}

发送端:

udp_sendpp.c

#include 
#include 
#include 
#include 
#include 
int main(int argc,char **argv)
{
	if(argc < 3)
	{
		perror("param must >= 3");
		return -1;
	}

	int cid = socket(AF_INET,SOCK_DGRAM,0);
	if(cid < 0)
	{
		perror("socket error");
		return -1;
	}

	struct sockaddr_in addr = {0},sendaddr = {0};
	addr.sin_family = AF_INET;
	addr.sin_port = htons(atoi(argv[2]));
	addr.sin_addr.s_addr = inet_addr(argv[1]);

	int size = sizeof(struct sockaddr);
   	char read_buf[128] = {0};
	char write_buf[128] = {0};
	int len;
	while(1)
	{
		bzero(read_buf,sizeof(read_buf));
		bzero(write_buf,sizeof(write_buf));
		printf("send:");
		fgets(write_buf,sizeof(write_buf),stdin);
	 len = sendto(cid,write_buf,strlen(write_buf),0,(struct sockaddr *)&addr,sizeof(struct sockaddr));
	   printf("send successful!!send len = %d\n",len);

		if(strcmp(write_buf,"quit\n") == 0)
		{
		   printf("you quit the communication!\n");
		   break;
		}
		recvfrom(cid,read_buf,sizeof(read_buf) - 1,0,(struct sockaddr *)&sendaddr,&size);
		if(strcmp(read_buf,"quit\n") == 0)
		{
		   printf("recv quit the communication!\n");
		   break;
		}
		printf("recv:%s",read_buf);
	}

	

	close(cid);
}

运行结果

发送端:
基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第5张图片

接收端:

基于Linux用C语言实现TCP半双工通信和UDP半双工通信_第6张图片


参考文章

https://blog.csdn.net/apollon_krj/article/details/53398448
https://zhuanlan.zhihu.com/p/24860273
https://www.cnblogs.com/lightice/p/12726717.html



本文结束,感谢您的阅读!

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