屏蔽ip网站吗北京微信小程序开发报价
web/
2025/10/1 6:13:26/
文章来源:
屏蔽ip网站吗,北京微信小程序开发报价,wordpress加授权,临清网站制作公司目录
一、socket简介
二、socket编程接口函数介绍
2.1 socket()函数#xff08;创建socket#xff09;
2.2 bind()函数#xff08;绑定地址和端口#xff09;
2.3 listen()函数#xff08;设置socket为监听模式#xff09;
2.4 accept()函数#xff08;接受连接…目录
一、socket简介
二、socket编程接口函数介绍
2.1 socket()函数创建socket
2.2 bind()函数绑定地址和端口
2.3 listen()函数设置socket为监听模式
2.4 accept()函数接受连接
2.5 connect()函数 连接服务器
2.6 读数据函数
2.7 写数据函数
2.8 字节序转换函数
2.9 IP地址格式转换函数
三、面向数据报(UDP)的socket编程
3.1 UDP服务端编程和测试
3.2 UDP客户端编程和测试
3.3 select()函数 一、socket简介 现在的网络编程接口通常是socket很多文献中文翻译做“套接字”。用socket能够实现网络上的不同主机之间或同一主机的不同对象之间的数据通信。所以现在socket已经是一类通用通信接口的集合。 套接字(socket)是Linux下的一种进程间通信机制(socket IPC)在前面的内容中已经给大家提到过使用socket IPC可以使得在不同主机上的应用程序之间进行通信网络通信当然也可以是同一台主机上的不同应用程序。socket IPC通常使用客户端—服务器这种模式完成通信多个客户端可以同时连接到服务器中与服务器之间完成数据交互。 内核向应用层提供了socket接口对于应用程序开发人员来说我们只需要调用socket接口开发自己的应用程序即可。socket是应用层与TCP/IP协议通信的中间软件抽象层它是一组接口。在设计模式中socket其实就是一个门面模式它把复杂的TCP/IP协议隐藏在socket接口后面对用户来说一组简单的接口就是全部让 socket去组织数据以符合指定的协议。所以我们无需深入的去理解tcp/udp等各种复杂的TCP/IP 协议socket已经为我们封装好了我们只需要遵循socket的规定去编程写出的程序自然遵循tcp/udp标准的。 当前网络中的主流程序设计都是使用 socket 进行编程的因为它简单易用它还是一个标准BSD socket能在不同平台很方便移植比如你的一个应用程序是基于socket接口编写的那么它可以移植到任何实现BSD socket标准的平台比如Windows它也实现了一套基于socket的套接字接口又比如在国产操作系统中如RT-Thread它也实现了BSD socket标准的socket 接口。 大的类型可以分为网络socket和本地socket两种 本地Socket在Linux上包括Unix Domain Socket和Netlink两种。Unix Domain Socket主要用于进程间通信NetLink用于用户空间和内核空间通讯本文暂不做讨论网络Socket支持很多种不同的协议。 本文所述为网络编程基础内容没有深入、详细地介绍socket编程只是网络编程入门。网络编程是一门非常难、非常深奥的技能市面上有很多关于Linux/UNIX网络编程类书籍这些书籍专门介绍了网络编程相关知识内容而且书本非常厚可见内容之多、难点之多。对于从事网络编程开发相关工作就要深入学习、研究相关知识了。 注本文主要讲述基于第四版本的TCP/IP协议族中的TCP和UDP协议的网络编程。此处在后面没有特殊指名时所有的讨论仅限于IPv4网络的协议族和地址表示。
二、socket编程接口函数介绍 Socket接口提供了socket(2)、bind(2)、listen(2)、accept(2)、connect(2)以及sendto(2)/recvfrom(2)这样的函数接口。在符合要求的情况下也可以使用read/write系统调用对socket进行数据读写。使用socket接口需要包含两个头文件sys/types.h和sys/socket.h。 注“socket(2)”这样的表示形式是Unix文档中通行的表示方式socket是函数名字()表示这是一个函数括号中的2表示这个函数的手册位于手册页2中可以使用命令: man 2 socket来进行查看。 对于我们提到的Socket系列函数接口在Linux上基本的手册都在手册页2中POSIX兼容的解释在手册页3p中可以通过man 3p socket这样的命令进行查看。对于一些特有的高级操作和解释可能会在手册页7中比如man 7 socket可以看到一些Linux的Socket高级选项。 根据函数原型仔细阅读系统自带手册是一个好习惯。 2.1 socket()函数创建socket 在进行Socket通信之前一般调用socket(2)函数来创建一个Socket通信端点。socket(2)函数原型如下
int socket(int domain, int type, int protocol); socket()函数类似于open()函数它用于创建一个网络通信端点打开一个网络通信如果成功则返回一个网络文件描述符通常把这个文件描述符称为socket描述符socket descriptor这个socket描述符跟文件描述符一样后续的操作都有用到它把它作为参数通过它来进行一些读写操作。参数列表中 domain代表这个Socket所使用的地址类型用于通信的协议族对于TCP/IP协议来说通常选择AF_INET就可以了对于IPv4协议使用AF_INET也可以使用PF_INET。实际上这两个值是相等的但是实际上通常大部分人更习惯使用AF_INET。当然如果你的IP协议的版本支持IPv6那么可以选择AF_INET6。type代表了这个Socket的类型我们讨论范围是有面向流的(TCP)和面向数据报的(UDP)Socket。分别取值SOCK_STREAM和SOCK_DGRAM。protocol是协议类型对于我们的应用场景都取0即可表示为给定的通信域和套接字类型选择默认协议。 调用socket()与调用open()函数很类似调用成功情况下均会返回用于文件I/O的文件描述符只不过对于socket()来说其返回的文件描述符一般称为socket描述符。当不再需要该文件描述符时可调用close()函数来关闭套接字释放相应的资源。如果socket()函数调用失败则会返回-1并且会设置errno变量以指示错误类型。 创建TCP Socket
sock_fd socket(AF_INET, SOCK_STREAM, 0); 创建UDP Socket
sock_fd socket(AF_INET, SOCK_DGRAM, 0);
2.2 bind()函数绑定地址和端口 创建了Socket后可以调用bind(2)函数来将这个Socket绑定到特定的地址和端口上来进行通信。函数原型如下
int bind(int socket, const struct sockaddr *addr, socklen_t addrlen); bind()函数用于将一个IP地址或端口号与一个套接字进行绑定将套接字与地址进行关联。一般来讲会将一个服务器的套接字绑定到一个众所周知的地址——即一个固定的与服务器进行通信的客户端应用程序提前就知道的地址 包括 IP 地址和端口号。因为对于客户端来说它与服务器进行通信首先需要知道服务器的IP地址以及对应的端口号所以通常服务器的IP地址以及端口号都是众所周知的。 参数列表中 socket应该是一个指向Socket的有效文件描述符。address参数就是一个指向struct sockaddr结构的指针根据不同的协议可以有不同的具体结构对于IP地址就是struct sockaddr_in。但是在调用函数的时候需要强制转换一下这个指针避免警告。addrlen因为前面的地址可能有各种不同的地址结构所以此处应该指明使用的地址数据结构的长度。编程时直接取sizeof(struct sockaddr_in)即可。 对于参数address一般我们在使用的时候都会使用struct sockaddr_in结构体sockaddr_in和sockaddr是并列的结构占用的空间是一样的指向sockaddr_in的结构体的指针也可以指向sockadd的结构体并代替它而且sockaddr_in结构对用户将更加友好在使用的时候进行类型转换就可以了。 当bind(2)调用成功时返回0失败时返回-1这时需要检查errno值本文不就不一一列举了。 注意对于服务器程序一般需要显式bind(2)到特定端口这样客户程序才知道连到哪个端口访问服务。但是对于客户端程序一般的来说可以不用显式bind(2)协议栈会在发起通信是将Socket自动绑定到一个随机的可用端口上进行通信即可但是显式bind(2)也是可以的。 2.3 listen()函数设置socket为监听模式 基于TCP协议的服务器需调用listen(2)函数将其Socket设置成被动模式等待客户机的连接。listen()函数只能在服务器进程中使用让服务器进程进入监听状态等待客户端的连接请求listen()函数在一般在bind()函数之后调用在accept()函数之前调用该函数原型如下
int listen(int socket, int backlog); 参数中的socket与前面的函数都相同backlog是指等待连接的队列长度但是实际上的队列可能会大于这个数字通常都取5。参数backlog用来描述sockfd的等待连接队列能够达到的最大值。在服务器进程正处理客户端连接请求的时候可能还存在其它的客户端请求建立连接因为TCP连接是一个过程由于同时尝试连接的用户过多使得服务器进程无法快速地完成所有的连接请求。因此内核会在自己的进程空间里维护一个队列这些连接请求就会被放入一个队列中服务器进程会按照先来后到的顺序去处理这些连接请求这样的一个队列内核不可能让其任意大所以必须有一个大小的上限这个backlog参数告诉内核使用这个数值作为队列的上限。而当一个客户端的连接请求到达并且该队列为满时客户端可能会收到一个表示连接失败的错误本次请求会被丢弃不作处理。 调用成功返回0失败返回-1此时需要检测处理errno值。 注无法在一个已经连接的套接字即已经成功执行connect()的套接字或由accept()调用返回的套接字上执行listen()。
2.4 accept()函数接受连接 服务器调用listen()函数之后就会进入到监听状态等待客户端的连接请求TCP服务器使用accept()函数获取客户端的连接请求并建立连接。函数原型如下
int accept(int socket, struct sockaddr *addr, socklen_t *addrlen); 参数列表中socket和前面的函数都一样参数addr也是一样的结构但是此处是一个传出参数是用来返回值的在成功返回的时候如果这个指针非空这里将存储请求连接的客户端的地址和端口。参数addrlen应设置为addr所指向的对象的字节长度如果我们对客户端的IP地址与端口号这些信息不感兴趣 可以把arrd和addrlen均置为空指针NULL。 accept(2)调用成功返回一个有效的文件描述符此文件描述符指向成功与客户端建立连接可以进行数据交换的Socket。服务器程序使用这个文件描述符来与客户端进行后续的交互。调用失败则返回-1此时需要检测处理errno值。 accept()函数通常只用于服务器应用程序中如果调用accept()函数时并没有客户端请求连接等待连接队列中也没有等待连接的请求此时accept()会进入阻塞状态直到有客户端连接请求到达为止。当有客户端连接请求到达时accept()函数与远程客户端之间建立连接accept()函数返回一个新的套接字。这个套接字与socket()函数返回的套接字并不同socket()函数返回的是服务器的套接字以服务器为例而accept()函数返回的套接字连接到调用connect()的客户端服务器通过该套接字与客户端进行数据交互比如向客户端发送数据、或从客户端接收数据。所以理解accept()函数的关键点在于它会创建一个新的套接字其实这个新的套接字就是与执行 connect()客户端调用 connect()向服务器发起连接请求的客户端之间建立了连接这个套接字代表了服务 器与客户端的一个连接。如果 accept()函数执行出错将会返回-1并会设置 errno 以指示错误原因。 注意accept(2)会根据文件描述符的O_NONBLOCK标识设置阻塞模式和非阻塞模式。若是非阻塞模式的当返回是-1时必须检查errno值是否EAGAIN或者EWOULDBLOCK没有连接请求时直接返回。另外accept(2)会被信号中断这是正常的在其返回-1时应该检查errno是否为EINTR如果是被信号中断的程序一般需要重新启动accept(2)调用。 2.5 connect()函数 连接服务器 对于客户机使用TCP协议时在通讯前必须调用connect(2)连接到需要通信的服务器的特定通信端点后才能正确进行通信。对于使用UDP协议的客户机这个步骤是可选项。如果使用了connect(2)在此之后可以不需要指定数据报的目的地址而直接发送否则每次发送数据均需要指定数据报的目的地址和端口。函数原型如下
int connect(int socket, const struct sockaddr *addr, socklen_t addrlen); connect(2)的所有参数以及含义均和bind(2)完全相同参数addr指定了待连接的服务器的IP地址以及端口号等信息参数addrlen指定了addr指向的struct sockaddr对象的字节大小。函数执行成功返回0失败返回-1此时需要检测处理errno值。 该函数用于客户端应用程序中客户端调用connect()函数将套接字sockfd与远程服务器进行连接客户端通过connect()函数请求与服务器建立连接对于TCP连接来说调用该函数将发生TCP连接的握手过程并最终建立一个TCP连接而对于UDP协议来说调用这个函数只是在sockfd中记录服务器IP地址与端口号而不发送任何数据。 函数调用成功则返回 0失败返回-1并设置errno以指示错误原因。
2.6 读数据函数 以下函数均可读取Socket数据read(2)、recv(2)、recvfrom(2)和recvmsg(2)。函数原型分别如下
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); 其中read(2)函数一般用于流式Socket简单读写数据也就是对应TCP协议。和普通文件的read(2)操作并无不同。当然也可以用于进行过connect(2)操作的UDP Socket文件描述符。 recv(2)函数与read(2)基本相同但是多一个参数flags这是一个专门用于读Socket数据的函数支持很多Socket的标识flags可以组合见表1。 recvfrom(2)函数相对于recv(2)增加两个参数用来返回接收到的数据的源地址这两个参数的形式和含义都与accept(2)中的后两个参数相同。如果这两个指针被置为NULL则recvfrom(2)的表现和recv(2)相同。 recvmsg(2)函数则是使用一个struct msghdr的结构来简化了参数本文不深入了解。
表1 接收数据标识 名称含义备注MSG_CMSG_CLOEXEC将接收数据的文件描述符设置标识close-on-exec只用于recvmsg(2)且从Linux 2.6.23才开始支持MSG_DONTWAIT以非阻塞方式读数据如果无数据可读则返回-1并设置errno为EAGAIN或者EWOULDBLOCK相当于将Socket设置为非阻塞模式从Linux 2.2开始支持MSG_ERRQUEUE如果Socket队列中有错误接收这个错误协议相关从Linux 2.2开始支持MSG_OOB处理带外数据MSG_PEEK读取队列头部数据但不清除这会导致下一次读操作读到相同的数据MSG_TRUNC即使缓冲区长度不够也返回真实的数据包长度从Linux 2.2开始支持其中Raw Socket(AF_PACKET)从Linux2.4.27/2.6.8开始支持此特性Netlink从Linux 2.6.22开始支持Unix数据报从Linux 3.4开始支持MSG_WAITALL阻塞到所有的请求都被满足通常是填满请求的缓冲区长度才返回从Linux 2.2开始支持如果被信号中断、发生错误或者连接断开依然可能未填满缓冲区
2.7 写数据函数 相对应的write(2)、send(2)、sendto(2)和sendmsg(2)都可以发送数据到Socket。功能和原型都类似于读数据函数函数原型如下
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen); 其中参数含义基本与读数据函数相同不同的是sendto(2)的最后一个地址长度参数是值而读数据函数中recvfrom(2)的最后一个参数是指针。另外支持的flags是不同的见表2。
表2 发送数据标识 名称含义备注MSG_CONFIRM告诉链路层准发过程发生会收到对端成功的回应从Linux 2.3.15开始支持目前只在IPv4和IPv6实现。MSG_DONTROUTE不要经过网管只发送到直连主机一般用于诊断路由问题并且只对可以路由的协议起作用MSG_DONTWAIT和发数据类似非阻塞模式从Linux 2.2开始支持MSG_EOR终止一个记录只在像SOCK_SEQPACKET这样支持此概念的协议有用从Linux 2.2开始支持MSG_MORE尽可能多的发送数据对TCP就是累积足够多的数据后再发送UDP是生成尽可能大的数据报从Linux 2.4.4开始支持Linux2.6 支持UDPMSG_NOSIGNAL对面向流的Socket有连接在连接断开时不发送SIGPIPE信号依然会设置errno为EPIPEMSG_OOB发送带外数据只对支持此概念的协议有效
2.8 字节序转换函数 在网络应用中字节序是一个必须考虑的因素因为不同机器类型可能采用不同标准的字节序所以均须按照网络标准转化。网络传输的标准叫做网络字节序实际上是大端序。而我们常用的X86或者ARM往往都是小端序。 在网络编程中不应该假设自己程序运行的主机的字节序应当使用htonl/htons/ntohs/ntohl之类的函数来在网络字节序和主机字节序之间进行转换。其中h代表host就是本地主机的表示形式n代表network表示网络上传输的字节序s和l代表类型short和long。 ARM的字节序实际上是可配置的但是一般都配置为小端。 手工进行字节序的转换往往是不方便的对于可移植的程序来说更是如此。总是需要知道自己的本地主机字节序也是很麻烦的。所以系统提供了四个固定的函数用来在本地字节序和网络字节序之间转换。这四个函数包含在头文件arpa/inet.h中他们的主要作用就是为了避免大小端的问题。分别是
uint32_t htonl(uint32_t hostlong); //32位整数从主机字节序转换为网络字节序
uint16_t htons(uint16_t hostshort); //16位整数从主机字节序转换为网络字节序
uint32_t ntohl(uint32_t netlong): //32位整数从网络字节序转换为主机字节序
uint16_t ntohs(uint16_t netshort); //16位整数从网络字节序转换为主机字节序
2.9 IP地址格式转换函数 在实际网络编程过程中往往需要在IP地址的点分十进制表示和二进制表示之间相互转化也需要进行主机名和地址的转换系统提供了一系列函数一般需要包含一下头文件netinet/in.h和arpa/inet.h。 我们更容易阅读的是点分十进制的IP地址比如192.168.1.136这是 一种字符串的形式但是计算机所需要理解的是二进制形式的IP地址所以就需要在点分十进制字符串和二进制地址之间进行转换。点分十进制字符串和二进制地址之间的转换函数主要有inet_aton、inet_addr、inet_ntoa、inet_ntop、 inet_pton这五个。
in_addr_t inet_addr(const char *cp) 这个函数将一个点分十进制的IP地址字符串转换成in_addr_t类型该类型实际上是一个32位无符号整数事实上就是前文提到的struct in_addr结构中的s_addr域的数据类型。注意这个二进制表示的IP地址规定是网络字节序。
char *inet_ntoa(struct in_addr in) 此函数可以将结构struct in_addr中的二进制IP地址转换为一个点分十进制表示的字符串返回这个字符串的首指针。使用起来很方便。但是要注意它返回的缓冲区是静态分配的在并发或者异步使用时要小心可能缓冲区随时可能被其它调用改写。
三、面向数据报(UDP)的socket编程 UDPUser Datagram Protocol用户数据报协议是一个面向数据报的传输层协议。UDP的传输是不可靠的简单的说就是发了不管发送者不会知道目标地址的数据通路是否发生拥塞也不知道数据是否到达是否完整以及是否还是原来的次序。它同TCP一样有用来标识本地应用的端口号。所以应用UDP的应用都能够容忍一定数量的错误和丢包。
3.1 UDP服务端编程和测试 面向数据报的UDP服务器基本流程如下图所示创建Socket后调用bind(2)绑定到特定接口就可以直接用这个套接字进行收发数据了。服务器需要使用recvfrom(2)这样的接口来接收数据并获取数据源地址和端口然后使用sendto(2将数据根据记录的数据源地址和端口回发即完成一次服务。这个UDP服务器除了检测到错误异常退出外始终在这个循环中运行。 图1 UDP服务器流程图 面向数据报的服务器使用UDP协议不像TCP服务器那样复杂每个客户端有单独的连接所以为了并发需要使用子进程或者线程和I/O多路复用。UDP协议没有连接状态只需要记住消息的来源直接在服务器Socket上读取并回发消息即可。 这里就不能简单像处理普通文件一样读写数据了需要在接收数据的时候使用recvfrom(2)函数这个函数会把数据报的源地址和端口结构以及该数据结构的长度在后面两个参数返回。我们记录这个地址。并使用sendto(2)函数将数据报回发到来源地址和端口即可。 UDP的服务器程序结构大大简化了只需要建立Socket并绑定到相应端口就可以收发数据了并且很容易对多个客户机并发。依据上述流程图编写UDP服务端测试程序udp_server_test.c交叉编译后下载到开发板上运行具体代码如下
#includestdio.h
#includesys/types.h
#includesys/socket.h
#includenetinet/in.h
#includeunistd.h
#includeerrno.h
#includestring.h
#includestdlib.h#define SERVER_PORT 4321int main()
{int sock_fd; //套接字描述符int recv_num;int send_num;char recv_buf[20] {0};char ack_buf[20] rev success;struct sockaddr_in addr_server; //服务端地址struct sockaddr_in addr_client; //客户端地址socklen_t client_addrlen;//AF_INET指定通信协议族(IPV4、IPV6等),SOCK_DGRAM表示socket类型(面向流的TCP取SOCK_STREAM面向数据报的UDP取SOCK_DGRAM)//第三个参数protocol通常设置为0表示为给定的通信域和套接字类型选择默认协议sock_fd socket(AF_INET,SOCK_DGRAM,0); //创建套接字文件描述符调用成功返回有效文件描述符失败返回-1并设置errno if(sock_fd 0){perror(socket error);exit(1);} else{printf(creat udp socket sucess\n);}//初始化服务器端地址memset(addr_server,0,sizeof(struct sockaddr_in));addr_server.sin_family AF_INET; //协议族addr_server.sin_port htons(SERVER_PORT); //端口号htons转换字节序// addr_serv.sin_addr.s_addr htonl(INADDR_ANY); //任意本地址,服务器所有网卡IP地址addr_server.sin_addr.s_addrinet_addr(192.168.2.136); //IP地址字符串格式转换为二进制格式client_addrlen sizeof(struct sockaddr_in);//绑定套接字if(bind(sock_fd,(struct sockaddr *)addr_server,sizeof(struct sockaddr_in))0 ) //第二个参数需要强转,成功返回0失败返回-1{perror(bind error);exit(1);} else{ printf(bind sucess\n);}while(1) //循环接收发送数据{printf(begin recv:\n);recv_num recvfrom(sock_fd,recv_buf,sizeof(recv_buf),0,(struct sockaddr *)addr_client,client_addrlen);if(recv_num 0){printf(recvfrom error\n);perror(again recvfrom);exit(1);} else{printf(recvfrom sucess,data len is %d:%s\n,recv_num,recv_buf);printf(begin send to ack:\n);send_num sendto(sock_fd,ack_buf,sizeof(ack_buf),0,(struct sockaddr *)addr_client,client_addrlen);if(send_num 0){perror(sendto);exit(1);}else{printf(send ack success:%s\n,ack_buf);}} }close(sock_fd);return 0;
} 程序遵循以下流程 ① 建立套接字文件描述符使用函数socket()生成套接字文件描述符。 ②设置服务器IP地址和端口初始化要绑定的网络地址结构。 ③绑定IP地址、端口等信息使用bind()函数将套接字文件描述符和一个地址进行绑定。 ④循环接收客户端的数据使用recvfrom()函数接收客户端的网络数据。 ⑤向客户端发送数据使用sendto()函数向服务器主机发送数据。 ⑥关闭套接字使用close()函数释放资源。UDP协议的客户端流程。 注其中代码44行bind的调用并不复杂关键在于需要对地址结构体指针进行转换否则编译器会给出警告。绑定的地址和端口在36到40行填充sturct sockaddr_in结构完成的服务器没有特殊要求的情况下绑定地址用INADDR_ANY监听所有地址即可另外要注意字节序的转换这对于程序尤其是要求可移植性的程序是一定要注意的。 测试环境搭建PC机上使用SocketTool软件模拟UDP客户端新建客户端设置对方IP为192.168.2.136对方端口为4321也就是开发板服务端IP和端口对应程序中绑定的IP和端口 。PC机修改IP地址为192.168.2.100保证与开发板在同一网段开发板通过网线与PC机连接。至此测试环境搭建完成接下来只需要在SocketTool刚刚新建的客户端上发送数据开发板就能收到了。 设置重复发送10次间隔1s如下图所示 图2 SocketTool模拟UDP客户端设置 开发板上运行程序完整的测试过程如下 UDP服务端socket编程测试结果 3.2 UDP客户端编程和测试 对于UDP客户机bind(2)和connect(2)都不是必须的系统会自动隐式处理这两个过程。创建Socket后可直接使用sendto(2)发送数据到服务器之后在这个Socket等待服务器回发的数据即可但是因为UDP可能会丢包需要设置超时超时后还未数据到来则判断数据报已经丢失。此时应该报告超时后退出而不应该始终等待下去造成程序卡死。 图3 UDP客户机流程图 UDP客户端的结构也很简单创建Socket后完全省略了bind(2)和connect(2)的步骤直接调用sendto(2)发送数据到服务器并等待回应即可。因为UDP有丢包可能客户端如果不设置超时会在丢包时卡死本例程调用select()函数设置超时超时未接收到数据则认为数据丢失。具体实现代码如下
#includestdio.h
#includestring.h
#includeerrno.h
#includestdlib.h
#includeunistd.h
#includesys/types.h
#includesys/socket.h
#includesys/select.h
#includenetinet/in.h#define DEST_PORT 60000
#define DSET_IP_ADDRESS 192.168.2.100int main()
{int sock_fd; //套接字文件描述符int send_num;int recv_num;int i;socklen_t dest_len;char send_buf[20] hello yrr;char recv_buf[20] {0};struct sockaddr_in addr_server; //服务端地址fd_set sockset; //定义文件描述符集合int ret;// struct timeval timeout { //设置阻塞等待时间为3s// .tv_sec 3,// };struct timeval timeout;sock_fd socket(AF_INET,SOCK_DGRAM,0);//创建套接字//初始化服务器端地址memset(addr_server,0,sizeof(addr_server));addr_server.sin_family AF_INET;addr_server.sin_addr.s_addr inet_addr(DSET_IP_ADDRESS);addr_server.sin_port htons(DEST_PORT);dest_len sizeof(struct sockaddr_in);printf(start send data 10 times:\n);for(i0;i10;i){send_num sendto(sock_fd,send_buf,sizeof(send_buf),0,(struct sockaddr *)addr_server,dest_len);if(send_num 0){perror(sendto);exit(1);} else //发出数据后等待服务端回复数据{printf(send success:%s ,waitting for server ack\n,send_buf);FD_ZERO(sockset); //初始化文件描述符集合sockset为空FD_SET(sock_fd,sockset); //添加UDP文件描述符timeout.tv_sec 3; //必须每次都要重新赋值否则下次时间为0不等待就返回timeout.tv_usec 0; //不能少否则一直阻塞ret select(sock_fd1,sockset,NULL,NULL,timeout);//阻塞直到文件描述符有数据可读才会返回timeout设置阻塞时间if(ret0) //返回-1表示有错误发生并设置errno{perror(select err);exit(1);}else if(ret 0) //返回0表示在任何文件描述符成为就绪态之前select()调用已经超时{printf(waitting for server ack timeout\n);}else //返回正值表示处于就绪态的文件描述符的个数可读了{printf(ret is %d,sock_fd have data to be read:\n,ret);if(FD_ISSET(sock_fd,sockset)) //判断sock_fd文件描述符是否是集合中的成员{memset(recv_buf,0,sizeof(recv_buf));recv_num recvfrom(sock_fd,recv_buf,sizeof(recv_buf),0,(struct sockaddr *)addr_server,dest_len);if(recv_num 0){perror(recvfrom);exit(1);} else{printf(recvfrom success:%s\n,recv_buf);}}}}}close(sock_fd);return 0;
} 程序遵循以下流程 ①建立套接字文件描述符socket() ②初始化填充服务器IP地址和端口struct sockaddr_in ③循环10次向服务器发送数据sendto() ④发送成功后调用select()函数阻塞等待如果有数据到来就recvfrom()正常读取数据超时未收到数据则认为数据丢失继续下一次循环。 ⑤关闭套接字close()。 53行超时设置为3秒。在55到83行是使用select(2)等待数据到来的代码当select(2)出错时需要判断errno如果正常等到数据到来则正常读取数据如果超时时间到并未收到数据则认为数据已经丢失继续下一回合通信。 测试环境搭建PC机上使用UDP_tester工具模拟UDP服务端设置服务器端口为60000PC机IP地址为192.168.2.100程序需对应。开发板IP地址为192.168.2.136保证与PC机在同一网段开发板通过网线与PC机连接至此测试环境搭建完成。 测试结果开发板上运行程序循环10次发送字符串“hello yrr”到服务端UDP_tester工具收到后会显示对方客户端IP和端口信息如下图所示 图4 UDP_tester模拟服务端接收数据 随后在工具下方窗口填写目的IP地址和端口发送“jdp”如下图所示服务端就会发送数据到开发板可在控制台看到相关打印信息。 图5 UDP_tester模拟服务端发送数据 完整测试过程如下 UDP客户端socket编程测试结果 结果分析可以看到服务器若不在3s之内回送数据到客户端select()函数会超时返回运用UDP_tester工具手动回送数据可在开发板控制台上观察到接收到的数据打印。 测试中发现的问题 ①用SocketTool模拟服务端时接收不到开发板发来的数据使用Wireshark抓包软件能抓到数据发到PC机上了但不知何缘故SocketTool就是收不到。后用UDP_tester工具就可以接收到开发板发来的数据。 ②调用select()函数设置超时退出时间为3s一开始是代码26-28行定义timeout并初始化3s循环调用时第一次不发数据是3s之后返回后面9次均是瞬间退出没有等待3s。后改进为代码53行每次循环开始重新赋值timeout的秒不赋值微秒现象依旧。后也重新赋值微秒才达到了每次等待3s的效果。本人亲测使用select()函数可能会遇见上述问题具体原因参考下一节对该函数的介绍。
3.3 select()函数 何为I/O多路复用 I/O多路复用IO multiplexing它通过一种机制可以监视多个文件描述符一旦某个文件描述符也就是某个文件可以执行I/O操作时能够通知应用程序进行相应的读写操作。I/O多路复用技术是为了解决在并发式I/O场景中进程或线程阻塞到某个I/O系统调用而出现的技术使进程不阻塞于某个特定的I/O系统调用。 由此可知I/O多路复用一般用于并发式的非阻塞I/O也就是多路非阻塞I/O比如程序中既要读取鼠标、又要读取键盘多路读取。 我们可以采用两个功能几乎相同的系统调用来执行I/O多路复用操作分别是系统调用select()和poll()。 这两个函数基本是一样的细节特征上存在些许差别 I/O多路复用存在一个非常明显的特征外部阻塞式内部监视多路 I/O。 系统调用select()可用于执行I/O多路复用操作调用select()会一直阻塞直到某一个或多个文件描述符成为就绪态可以读或写。使用该函数需要包含头文件sys/select.h其函数原型如下所示
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 参数列表中readfds、writefds以及exceptfds都是fd_set类型指针指向一个fd_set类型对象fd_set数据类型是一个文件描述符的集合体所以参数readfds、writefds以及exceptfds都是指向文件描述符集合的指针这些参数按照如下方式使用 readfds是用来检测读是否就绪是否可读的文件描述符集合 writefds是用来检测写是否就绪是否可写的文件描述符集合 exceptfds是用来检测异常情况是否发生的文件描述符集合。 Linux提供了四个宏用于对fd_set类型对象进行操作所有关于文件描述符集合的操作都是通过这四个宏来完成的FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()稍后介绍。 如果对readfds、writefds以及exceptfds中的某些事件不感兴趣可将其设置为NULL这表示对相应条件不关心。如果这三个参数都设置为NULL则可以将select()当做为一个类似于sleep()休眠的函数来使用通过select()函数的最后一个参数timeout来设置休眠时间。 select()函数的第一个参数nfds通常表示最大文件描述符编号值加1考虑readfds、writefds以及exceptfds这三个文件描述符集合在3个描述符集中找出最大描述符编号值然后加1这就是参数nfds。select()函数的最后一个参数timeout可用于设定select()阻塞的时间上限控制select的阻塞行为可将timeout参数设置为NULL表示select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态也可将其指向一个struct timeval结构体对象如果参数timeout指向的struct timeval 结构体对象中的两个成员变量都为 0那么此时select()函数不会阻塞它只是简单地轮训指定的文件描述符集合看看其中是否有就绪的文件描述符并立刻返回。否则参数timeout将为select()指定一个等待阻塞时间的上限值如果在阻塞期间内文件描述符集合中的某一 个或多个文件描述符成为就绪态将会结束阻塞并返回如果超过了阻塞时间的上限值select()函数将会返 回select()函数将阻塞直到有以下事情发生 ①readfds、writefds或exceptfds指定的文件描述符中至少有一个称为就绪态 ②该调用被信号处理函数中断 ③参数timeout中指定的时间上限已经超时。 文件描述符集合的所有操作都可以通过FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()这四个宏来完成这些宏定义如下所示
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set); 文件描述符集合有一个最大容量限制有常量FD_SETSIZE来决定在Linux系统下该常量的值为1024。在定义一个文件描述符集合之后必须用FD_ZERO()宏将其进行初始化操作然后再向集合中添加我们关心的各个文件描述符例如
fd_set fset; //定义文件描述符集合
FD_ZERO(fset); //将集合初始化为空
FD_SET(3, fset); //向集合中添加文件描述符 3
FD_SET(4, fset); //向集合中添加文件描述符 4在调用select()函数之后select()函数内部会修改readfds、writefds、exceptfds这些集合当select()函数返回时它们包含的就是已处于就绪态的文件描述符集合了。比如在调用select()函数之前readfds所指向的集合中包含了 3、4这两个文件描述符当调用select()函数之后假设select()返回时只有文件描述符4已经处于就绪态了那么此时readfds指向的集合中就只包含了文件描述符 4。所以由此可知如果要在循环中重复调用select()我们必须保证每次都要重新初始化并设置readfds、writefds、exceptfds这些集合。 select()函数有三种可能的返回值会返回如下三种情况中的一种 ①返回-1表示有错误发生并且会设置errno。可能的错误码包括EBADF、EINTR、EINVAL、EINVAL以及ENOMEMEBADF表示readfds、writefds或exceptfds中有一个文件描述符是非法的EINTR表示该函数被信号处理函数中断了其它错误可以自行网上查询在man手册都有详细的记录。 ②返回0表示在任何文件描述符成为就绪态之前select()调用已经超时在这种情况下readfdswritefds以及exceptfds所指向的文件描述符集合都会被清空。 ③返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数在这种情况下每个返回的文件描述符集合都需要检查通过 FD_ISSET()宏进行检查 以此找出发生的 I/O 事件是什么。如果同一个文件描述符在readfdswritefds以及exceptfds 中同时被指定且它多于多个I/O事件都处于就绪态的话那么就会被统计多次换句话说select()返回三个集合中被标记为就绪态的文件描述符的总数。 使用示例参考注释
int main()
{int ret;int buf[20];fd_set sockset; //定义文件描述符集合struct timeval timeout; //定义timeout设置阻塞时间……;……;……;while (1) {FD_ZERO(sockset); //初始化文件描述符集合sockset为空 FD_SET(0, sockset); //添加键盘FD_SET(fd, sockset); //添加鼠标,fd位鼠标文件描述符timeout.tv_sec 3; //必须每次都要重新赋值否则下次时间为0不等待就返回timeout.tv_usec 0; //不能少否则一直阻塞ret select(fd 1, sockset, NULL, NULL, timeout); //检测两个文件描述符是否可读if (ret 0) //select err{perror(select error);exit(-1);}else if (ret 0) //超时返回{fprintf(stderr, select timeout.\n);}else //返回正值表示处于就绪态的文件描述符的个数此处为可读{}if(FD_ISSET(0, sockset)) //检查键盘是否为就绪态是返回true否则返回false{ret read(0, buf, sizeof(buf));}if(FD_ISSET(fd, sockset)) //检查鼠标是否为就绪态是返回true否则返回false{ret read(0, buf, sizeof(buf));}}return 0;
}
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/84889.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!