计算机网络 - TCP 与 UDP
计算机网络 - TCP 与 UDP
TCP 与 UDP 介绍
TCP
TCP(Transmission Control Protocol 传输控制协议)是一种 面向连接的、可靠的、基于字节流 的传输层通信协议,由 IETF 的 RFC 793 定义。在简化的计算机网络 OSI 模型中,它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP 层是位于 IP 层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是 IP 层不提供这样的流机制,而是提供不可靠的包交换。
应用层向 TCP 层发送用于网间传输的、用 8 位字节表示的数据流,然后 TCP 把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后 TCP 把结果包传给 IP 层,由它来通过网络将包传送给接收端实体的 TCP 层。TCP 为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP 用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
在拥塞控制上,采用广受好评的 TCP 拥塞控制算法(也称 AIMD 算法)。该算法主要包括三个主要部分:1)加性增、乘性减;2)慢启动;3)对超时事件做出反应。
连接建立
TCP 是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出 SYN 连接请求后,等待对方回答 SYN+ACK 并最终对对方的 SYN 执行 ACK 确认。这种建立连接的方法可以防止产生错误的连接,TCP 使用的流量控制协议是可变大小的滑动窗口协议。
TCP 三次握手的过程如下:
- 客户端发送 SYN(SEQ=x) 报文给服务器端,进入 SYN_SEND 状态。
- 服务器端收到 SYN 报文,回应一个 SYN(SEQ=y) ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
- 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1)报文,进入 Established 状态。
三次握手完成,TCP 客户端和服务器端成功地建立连接,可以开始传输数据了。
连接终止
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由 TCP 的半关闭(half-close)造成的。具体过程如下图所示。
- 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
- 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
注意:FIN 的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。 - 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
- 接收这个最终 FIN 的原发送端 TCP(即执行主动关闭的那一端)确认这个 FIN。
既然每个方向都需要一个 FIN 和一个 ACK,因此通常需要 4 个分节。
UDP
UDP 是 User Datagram Protocol(用户数据报协议)的简称,是 OSI(Open System Interconnection,开放式系统互联) 参考模型中一种 无连接 的传输层协议,提供面向事务的简单不可靠信息传送服务。
UDP 协议全称是用户数据报协议,在网络中它与 TCP 协议一样用于处理数据包,是一种无连接的协议。在 OSI 模型中,在第四层——传输层,处于 IP 协议的上一层。UDP 有 不提供 数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP 用来支持那些需要在计算机之间传输数据的网络应用。包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都需要使用 UDP 协议。UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是即使是在今天 UDP 仍然不失为一项非常实用和可行的网络传输层协议。
与所熟知的 TCP(传输控制协议)协议一样,UDP 协议直接位于 IP(网际协议)协议的顶层。根据 OSI(开放系统互连)参考模型,UDP 和 TCP 都属于传输层协议。UDP 协议的主要作用是将网络数据流量压缩成数据包的形式。一个典型的数据包就是一个二进制数据的传输单位。每一个数据包的前 8 个字节用来包含报头信息,剩余字节则用来包含具体的传输数据。
UDP 是 OSI 参考模型中一种无连接的传输层协议,它主要用于 不要求分组顺序到达 的传输中,分组传输顺序的检查与排序由应用层完成,提供 面向事务的简单不可靠信息传送服务 。UDP 协议基本上是 IP 协议与上层协议的接口。UDP 协议适用端口分别运行在同一台设备上的多个应用程序。
总结
-
TCP与UDP区别总结:
-
TCP 面向连接(如打电话要先拨号建立连接); UDP 是无连接的,即发送数据之前不需要建立连接
-
TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付,TCP 通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
-
UDP 具有较好的实时性,工作效率比 TCP 高,适用于对高速传输和实时性有较高的通信或广播通信。
-
每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
-
TCP 对系统资源要求较多,UDP 对系统资源要求较少。
-
-
为什么 UDP 有时比 TCP 更有优势?
-
UDP 以其简单、传输快的优势,在越来越多场景下取代了 TCP,如实时游戏。
-
网速的提升给 UDP 的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。
-
TCP 为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于 TCP 内置的系统协议栈中,极难对其进行改进。
-
采用 TCP,一旦发生丢包,TCP 会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于 UDP 对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。
-
报文
TCP 报文段头
UDP 报文头
使用 Socket 编程
套接字是通信端点的抽象,其英文 socket,即为插座,孔的意思。如果两个机子要通信,中间要通过一条线,这条线的两端要连接通信的双方,这条线在每一台机子上的接入点则为 socket,即为插孔,所以在通信前,我们在通信的两端必须要建立好这个插孔,同时为了保证通信的正确,端和端之间的插孔必须要一一对应,这样两端便可以正确的进行通信了,而这个插孔对应到我们实际的操作系统中,就是 socket 文件,我们再创建它之后,就会得到一个操作系统返回的对于该文件的描述符,然后应用程序可以通过使用套接字描述符访问套接字,向其写入输入,读出数据。
站在更贴近系统的层级去看,两个机器间的通信方式,无非是要通过运输层的TCP/UDP,网络层IP,因此socket本质是编程接口(API),对TCP/UDP/IP的封装,TCP/UDP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。
1、Socket 的创建
#include <sys/socket.h>
int socket (int domain, int type, int protocol);
//
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
这样,我们便创建了一个socket,对于socket接收的参数都有什么意义呢?从上面,我们可以知道socket是对于底层网络通信的一个封装,而对于底层的网络通信也是具备多种类型的。而这些参数则是通过组合来表示各类通信的特征,从而建立正确的套接字。
- domain: 通信的特性,每个域有自己的地址表示格式,AF打头,表示地址族(Address family)
- type:套接字的类型,进一步确定通信特征。
- protocol: 表示为给定域和套接字类型选择默认协议,当对同一域和套接字类型支持多个协议时,可以通过该字段来选择一个特定协议,通常默认为 0。上面设置的 socket 类型,在执行的时候也会有默认的协议类型提供,比如 SOCK_STREAM 就 TCP 协议。
- SOCK_STREAM 这种是 Transmission Control Protocol 传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,即每次收发数据之前必须通过 connect 建立连接
- SOCK_DGRAM 这种是 User Datagram Protocol 协议的网络通讯,是一种 无连接 的传输层协议,提供面向事务的简单 不可靠 信息传送服务。
- SOCK_RAW 套接字提供一个数据报接口。通过这个我们可以直接访问下面的网络层,绕过 TCP/UDP,因此我们可以进行制定自己的传输层协议。
2、Socket 的关闭
- 当我们不再使用 Socket 的时候,我们可以调用 close 函数来将其关闭,释放该文件描述符,这样便可以得到重新的使用。
- 套接字通信是双向的,但是,我们可以采用 shutdown 函数来禁止一个套接字的 I/O。
#include<sys/socket.h>
int shutdown(int sockfd, int how);
how 可以用来指定读端口或者是写端口,这样我们便可以关闭掉读端或者写端。
3、字节序
字节序是处理器架构的特性,用来指示像整数这种数据类型的内部如何排序,大端和小端,因此如果通信双方的处理器架构不同,则会导致字节序的不一致的问题出现。最底层的网络协议指定了字节序,大端字节序,但是应用程序在处理数据时,则会遇到字节序不一致的问题。对此,系统提供了进行处理器字节序和网络字节序之间实施转换的函数。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32)//主机字节转化为网络字节序
uint16_t htons(uint16_t hostint16)
uint32_t ntohl(uint32_t netint32)//网络字节序转化为主机字节序
unint16_t ntohs(uint16_t netint16)
4、地址格式
如何表示一个要通信的进程,需要一个网络地址和端口,而在系统中如何具体的标示这一特征呢?根据之前 socket 的创建,我们知道不同socket 对应了不同的通信特征,而对于不同的通信特征,其地址表示上也有一些差别。
这里我们只看一下 IPV4 因特网域地址的表示结构。
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
}
- sin_family: 通信的的域,这里为 AF_INET: IPV4 因特网域
- sin_port: 通信的端口
- sin_addr: 网络地址
/** 255.255.255.255 */
#define IPADDR_NONE ((u32_t)0xffffffffUL)
/** 127.0.0.1 */
#define IPADDR_LOOPBACK ((u32_t)0x7f000001UL)
/** 0.0.0.0 */
#define IPADDR_ANY ((u32_t)0x00000000UL)
/** 255.255.255.255 */
#define IPADDR_BROADCAST ((u32_t)0xffffffffUL)
5、socket 选项设置
对于 Socket,系统提供了更具体细致化的一些配置选项,通过这些配置选项,我们可以进行进一步具体的配置。
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int option, const void *val, socklen_t len);
- sockfd: 我们要进行配置的 socket
- level: 根据我们选用的协议,配置相应的协议编号
- option: 选项即为下表
- val: 用来存放返回值
Socket API
#if LWIP_COMPAT_SOCKETS
#define accept(a,b,c) lwip_accept(a,b,c)
#define bind(a,b,c) lwip_bind(a,b,c)
#define shutdown(a,b) lwip_shutdown(a,b)
#define closesocket(s) lwip_close(s)
#define connect(a,b,c) lwip_connect(a,b,c)
#define getsockname(a,b,c) lwip_getsockname(a,b,c)
#define getpeername(a,b,c) lwip_getpeername(a,b,c)
#define setsockopt(a,b,c,d,e) lwip_setsockopt(a,b,c,d,e)
#define getsockopt(a,b,c,d,e) lwip_getsockopt(a,b,c,d,e)
#define listen(a,b) lwip_listen(a,b)
#define recv(a,b,c,d) lwip_recv(a,b,c,d)
#define recvfrom(a,b,c,d,e,f) lwip_recvfrom(a,b,c,d,e,f)
#define send(a,b,c,d) lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f) lwip_sendto(a,b,c,d,e,f)
#define socket(a,b,c) lwip_socket(a,b,c)
#define select(a,b,c,d,e) lwip_select(a,b,c,d,e)
#define ioctlsocket(a,b,c) lwip_ioctl(a,b,c)
#if LWIP_POSIX_SOCKETS_IO_NAMES
#define read(a,b,c) lwip_read(a,b,c)
#define write(a,b,c) lwip_write(a,b,c)
#define close(s) lwip_close(s)
socket() --得到文件描述符!
bind() --我们在哪个端口?
connect() --Hello!
listen() --有人给我打电话吗?
accept() --“Thank you for calling port 3490.”
send() 和 recv() --Talk to me, baby!
sendto() 和 recvfrom() --Talk to me, DGRAM-style
close() 和 shutdown() --滚开!
getpeername() --你是谁?
gethostname() --我是谁?
DNS --你说“白宫”,我说 “198.137.240.100”
Select
select 系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。
1、阻塞模式
int iResult = recv(s, buffer,1024);
这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永 远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。
2、非阻塞模式
int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);
这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。
select模型的出现就是为了解决上述问题。
如上所示,用户首先将需要进行 IO 操作的 socket 添加到 select 中,然后阻塞等待select系统调用返回。当数据到达时,socket 被**,select 函数返回。用户线程正式发起 read 请求,读取数据并继续执行。
从流程上来看,使用 select 函数进行 IO 请求和同步阻塞模型没有太大的区别,甚至还多了添加监视 socket,以及调用 select 函数的额外操作,效率更差。但是,使用 select以后最大的优势是用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 select 读取被**的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
select 流程伪代码如下:
{
select(socket);
while(1)
{
sockets = select();
for(socket in sockets)
{
if(can_read(socket))
{
read(socket, buffer);
process(buffer);
}
}
}
}
Select 相关 API 介绍与使用
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
// select函数相关的常见的几个宏:
int FD_ZERO(int fd, fd_set *fdset); //一个 fd_set类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); //清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); //设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位
参数说明:
- maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的;
- readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。
- timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间
- 返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。
Select 使用范例:
- 当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位。
- 然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。
- select返回后,用FD_ISSET测试给定位是否置位:
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
fd_set rd;
struct timeval tv;
int err;
FD_ZERO(&rd);
FD_SET(0,&rd);
tv.tv_sec = 5;
tv.tv_usec = 0;
err = select(1,&rd,NULL,NULL,&tv);
if(err == 0) //超时
{
printf("select time out!\n");
}
else if(err == -1) //失败
{
printf("fail to select!\n");
}
else //成功
{
printf("data is available!\n");
}
return 0;
}
深入理解 Select 模型
理解 select 模型的关键在于理解 fd_set,为说明方便,取 fd_set 长度为 1 字节,fd_set 中的每一 bit 可以对应一个文件描述符 fd。则 1 字节长的 fd_set 最大可以对应 8 个 fd。
(1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
基于上面的讨论,可以轻松得出select模型的特点:
(1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
(3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。
例程
TCP Server
TCP 编程的服务器端一般步骤是:
1、创建一个 socket,用函数 socket();
2、设置 socket 属性,用函数 setsockopt(); 可选
3、绑定 IP 地址、端口等信息到 socket 上,用函数 bind();
4、开启监听,用函数 listen();
5、接收客户端上来的连接,用函数 accept();
6、收发数据,用函数 send() 和 recv(),或者 read() 和 write();
7、关闭网络连接; closesocket();
8、关闭监听。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define PORT 22468
#define KEY 123
#define SIZE 1024
int main()
{
char buf[100];
memset(buf,0,100);
int server_sockfd,client_sockfd;
socklen_t server_len,client_len;
struct sockaddr_in server_sockaddr,client_sockaddr;
/*create a socket.type is AF_INET,sock_stream*/
server_sockfd = socket(AF_INET,SOCK_STREAM,0);
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
server_len = sizeof(server_sockaddr);
int on;
setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,&on,sizeof(on));
/*bind a socket or rename a sockt*/
if(bind(server_sockfd, (struct sockaddr*)&server_sockaddr, server_len)==-1){
printf("bind error");
exit(1);
}
if(listen(server_sockfd, 5)==-1){
printf("listen error");
exit(1);
}
client_len = sizeof(client_sockaddr);
pid_t ppid,pid;
while(1) {
if((client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_sockaddr, &client_len)) == -1){
printf("connect error");
exit(1);
} else {
printf("create connection successfully\n");
int error = send(client_sockfd, "You have conected the server", strlen("You have conected the server"), 0);
printf("%d\n", error);
}
}
return 0;
}
TCP Client
TCP 编程的客户端一般步骤是:
1、创建一个 socket,用函数 socket();
2、设置 socket 属性,用函数 setsockopt(); 可选
3、绑定 IP 地址、端口等信息到 socket 上,用函数bind(); 可选
4、设置要连接的对方的 IP 地址和端口等属性;
5、连接服务器,用函数 connect();
6、收发数据,用函数 send() 和 recv(),或者 read() 和 write();
7、关闭网络连接;
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_PORT 22468
#define MAXDATASIZE 100
#define SERVER_IP "Your IP"
int main() {
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct sockaddr_in server_addr;
printf("\n======================client initialization======================\n");
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
bzero(&(server_addr.sin_zero),sizeof(server_addr.sin_zero));
if (connect(sockfd, (struct sockaddr *)&server_addr,sizeof(struct sockaddr_in)) == -1){
perror("connect error");
exit(1);
}
while(1) {
bzero(buf,MAXDATASIZE);
printf("\nBegin receive...\n");
if ((numbytes = recv(sockfd, buf, MAXDATASIZE, 0)) == -1){
perror("recv");
exit(1);
} else if (numbytes > 0) {
int len, bytes_sent;
buf[numbytes] = '\0';
printf("Received: %s\n",buf);
printf("Send:");
char msg[100];
scanf("%s",msg);
len = strlen(msg);
//sent to the server
if(send(sockfd, msg,len,0) == -1){
perror("send error");
}
} else {
printf("soket end!\n");
break;
}
}
close(sockfd);
return 0;
}
UDP Server
UDP 编程的服务器端一般步骤是:
1、创建一个 socket,用函数 socket();
2、设置 socket属性,用函数 setsockopt(); 可选
3、绑定 IP 地址、端口等信息到 socket上,用函数bind();
4、循环接收数据,用函数 recvfrom();
5、关闭网络连接;
static void udp_server_task(void *pvParameters)
{
char rx_buffer[128];
char addr_str[128];
int addr_family;
int ip_protocol;
while (1) {
#ifdef CONFIG_EXAMPLE_IPV4
struct sockaddr_in destAddr;
destAddr.sin_addr.s_addr = htonl(INADDR_ANY);
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
inet_ntoa_r(destAddr.sin_addr, addr_str, sizeof(addr_str) - 1);
#else // IPV6
struct sockaddr_in6 destAddr;
bzero(&destAddr.sin6_addr.un, sizeof(destAddr.sin6_addr.un));
destAddr.sin6_family = AF_INET6;
destAddr.sin6_port = htons(PORT);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
inet6_ntoa_r(destAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
#endif
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
int err = bind(sock, (struct sockaddr *)&destAddr, sizeof(destAddr));
if (err < 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
}
ESP_LOGI(TAG, "Socket binded");
while (1) {
ESP_LOGI(TAG, "Waiting for data");
struct sockaddr_in6 sourceAddr; // Large enough for both IPv4 or IPv6
socklen_t socklen = sizeof(sourceAddr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&sourceAddr, &socklen);
// Error occured during receiving
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
break;
}
// Data received
else {
// Get the sender's ip address as string
if (sourceAddr.sin6_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&sourceAddr)->sin_addr.s_addr, addr_str, sizeof(addr_str) - 1);
} else if (sourceAddr.sin6_family == PF_INET6) {
inet6_ntoa_r(sourceAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
}
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string...
ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
ESP_LOGI(TAG, "%s", rx_buffer);
int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&sourceAddr, sizeof(sourceAddr));
if (err < 0) {
ESP_LOGE(TAG, "Error occured during sending: errno %d", errno);
break;
}
}
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
UDP Client
UDP 编程的客户端一般步骤是:
1、创建一个 socket,用函数 socket();
2、设置 socket 属性,用函数 setsockopt(); 可选
3、绑定 IP 地址、端口等信息到 socket 上,用函数 bind(); 可选
4、设置对方的 IP 地址和端口等属性;
5、发送数据,用函数 sendto();
6、关闭网络连接;
static void udp_client_task(void *pvParameters)
{
char rx_buffer[128];
char addr_str[128];
int addr_family;
int ip_protocol;
while (1) {
#ifdef CONFIG_EXAMPLE_IPV4
struct sockaddr_in destAddr;
destAddr.sin_addr.s_addr = inet_addr(HOST_IP_ADDR);
destAddr.sin_family = AF_INET;
destAddr.sin_port = htons(PORT);
addr_family = AF_INET;
ip_protocol = IPPROTO_IP;
inet_ntoa_r(destAddr.sin_addr, addr_str, sizeof(addr_str) - 1);
#else // IPV6
struct sockaddr_in6 destAddr;
inet6_aton(HOST_IP_ADDR, &destAddr.sin6_addr);
destAddr.sin6_family = AF_INET6;
destAddr.sin6_port = htons(PORT);
addr_family = AF_INET6;
ip_protocol = IPPROTO_IPV6;
inet6_ntoa_r(destAddr.sin6_addr, addr_str, sizeof(addr_str) - 1);
#endif
int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Socket created");
while (1) {
int err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&destAddr, sizeof(destAddr));
if (err < 0) {
ESP_LOGE(TAG, "Error occured during sending: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Message sent");
struct sockaddr_in sourceAddr; // Large enough for both IPv4 or IPv6
socklen_t socklen = sizeof(sourceAddr);
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&sourceAddr, &socklen);
// Error occured during receiving
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
break;
}
// Data received
else {
rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
ESP_LOGI(TAG, "Received %d bytes from %s:", len, addr_str);
ESP_LOGI(TAG, "%s", rx_buffer);
}
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
if (sock != -1) {
ESP_LOGE(TAG, "Shutting down socket and restarting...");
shutdown(sock, 0);
close(sock);
}
}
vTaskDelete(NULL);
}
参考链接
- https://segmentfault.com/a/1190000010838127
- https://blog.****.net/xiaobangkuaipao/article/details/76793702
- https://www.cnblogs.com/skyfsm/p/7079458.html
- https://blog.****.net/cstarbl/article/details/7645298
- https://www.cnblogs.com/hnrainll/archive/2011/07/22/2113745.html
- http://pubs.opengroup.org/onlinepubs/007908799/xnsix.html