计算机网络 - TCP 与 UDP

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_27114397/article/details/87916780

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 三次握手的过程如下:

  1. 客户端发送 SYN(SEQ=x) 报文给服务器端,进入 SYN_SEND 状态。
  2. 服务器端收到 SYN 报文,回应一个 SYN(SEQ=y) ACK(ACK=x+1)报文,进入 SYN_RECV 状态。
  3. 客户端收到服务器端的 SYN 报文,回应一个 ACK(ACK=y+1)报文,进入 Established 状态。

三次握手完成,TCP 客户端和服务器端成功地建立连接,可以开始传输数据了。

在这里插入图片描述

连接终止

建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由 TCP 的半关闭(half-close)造成的。具体过程如下图所示。

  1. 某个应用进程首先调用 close,称该端执行“主动关闭”(active close)。该端的 TCP 于是发送一个 FIN 分节,表示数据发送完毕。
  2. 接收到这个 FIN 的对端执行 “被动关闭”(passive close),这个 FIN 由 TCP 确认。
    注意:FIN 的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程,放在已排队等候该应用进程接收的任何其他数据之后,因为,FIN 的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用 close 关闭它的套接字。这导致它的 TCP 也发送一个 FIN。
  4. 接收这个最终 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 协议适用端口分别运行在同一台设备上的多个应用程序。

总结

  1. TCP与UDP区别总结:

    1. TCP 面向连接(如打电话要先拨号建立连接); UDP 是无连接的,即发送数据之前不需要建立连接

    2. TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付,TCP 通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。

    3. UDP 具有较好的实时性,工作效率比 TCP 高,适用于对高速传输和实时性有较高的通信或广播通信。

    4. 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信

    5. TCP 对系统资源要求较多,UDP 对系统资源要求较少。

  2. 为什么 UDP 有时比 TCP 更有优势?

    1. UDP 以其简单、传输快的优势,在越来越多场景下取代了 TCP,如实时游戏。

    2. 网速的提升给 UDP 的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。

    3. TCP 为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,由于 TCP 内置的系统协议栈中,极难对其进行改进。

    4. 采用 TCP,一旦发生丢包,TCP 会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,延时会越来越大,基于 UDP 对实时性要求较为严格的情况下,采用自定义重传机制,能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。

报文

TCP 报文段头

TCP 头

UDP 报文头

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)

domain

  • type:套接字的类型,进一步确定通信特征。

type

  • protocol: 表示为给定域和套接字类型选择默认协议,当对同一域和套接字类型支持多个协议时,可以通过该字段来选择一个特定协议,通常默认为 0。上面设置的 socket 类型,在执行的时候也会有默认的协议类型提供,比如 SOCK_STREAM 就 TCP 协议。
    • SOCK_STREAM 这种是 Transmission Control Protocol 传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,即每次收发数据之前必须通过 connect 建立连接
    • SOCK_DGRAM 这种是 User Datagram Protocol 协议的网络通讯,是一种 无连接 的传输层协议,提供面向事务的简单 不可靠 信息传送服务。
    • SOCK_RAW 套接字提供一个数据报接口。通过这个我们可以直接访问下面的网络层,绕过 TCP/UDP,因此我们可以进行制定自己的传输层协议。

protocol
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 使用范例:

  1. 当声明了一个文件描述符集后,必须用FD_ZERO将所有位置零。之后将我们所感兴趣的描述符所对应的位置位。
  2. 然后调用select函数,拥塞等待文件描述符事件的到来;如果超过设定的时间,则不再等待,继续往下执行。
  3. 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);
}

参考链接

猜你喜欢

转载自blog.csdn.net/qq_27114397/article/details/87916780