目录
在网络socket编程(一)中使用select()函数设置UDP接收数据超时时,发现了两个问题:
①设定一个超时时间,循环执行select()函数时第一次可以等待设置的时间,第二次及以后就会瞬间返回,根本不会等待,而且显示超时。程序是这样的:
int main()
{
int ret;
struct timeval timeout = { //设置阻塞等待时间为3s
.tv_sec = 3,
};
while(1)
{
FD_ZERO(&sockset); //初始化文件描述符集合sockset为空
FD_SET(sock_fd,&sockset); //添加UDP文件描述符
ret = select(sock_fd+1,&sockset,NULL,NULL,&timeout);//阻塞直到文件描述符有数据可读才会返回,timeout设置阻塞时间
if(ret<0) //返回-1表示有错误发生并设置errno
{
perror("select err");
exit(1);
}
else if(ret == 0) //返回0表示在任何文件描述符成为就绪态之前select()调用已经超时
{
printf("waitting for server ack timeout\n");
}
else //返回正值表示处于就绪态的文件描述符的个数,可读了
{
;
}
}
return 0;
}
通过打印返回值发现select()在第二次执行以后返回的就一直是0,表示超时;打印timeout中的秒数发现第二次以后就变为0了,相当于不等待,这就是造成此现象的原因了。为什么定义初始化的变量值会在执行一次select()后改变?
网上求助各位大佬看到这样一个解释:man手册查看相关信息说select()函数会更新超时参数以指示剩余多少时间,即timeout是上一次调用后剩余的时间。如果上次是超时退出的,那么下一次时间就为0。
所以解决办法就是每次执行select()函数前重置超时时间,代码如下:
while(1)
{
FD_ZERO(&sockset); //初始化文件描述符集合sockset为空
FD_SET(sock_fd,&sockset); //添加UDP文件描述符
timeout.tv_sec = 3; //必须每次都要重新赋值,否则下次时间为0不等待就返回
timeout.tv_usec = 0; //不能少,否则一直阻塞
ret = select(sock_fd+1,&sockset,NULL,NULL,&timeout);//阻塞直到文件描述符有数据可读才会返回,timeout设置阻塞时间
}
②一直阻塞,程序是这样的:
FD_ZERO(&sockset); //初始化文件描述符集合sockset为空
FD_SET(sock_fd,&sockset); //添加UDP文件描述符
timeout.tv_sec = 3; //必须每次都要重新赋值,否则下次时间为0不等待就返回
ret = select(sock_fd+1,&sockset,NULL,NULL,&timeout);//阻塞直到文件描述符有数据可读才会返回,timeout设置阻塞时间
此现象网上没有查到相关解释,本人也是测试时尝试对timeout的usec进行赋值,发现就正常实现每次执行select()阻塞3秒。 因此编程时需注意每次循环重置超时时间时对tv_sec和tv_usec成员都要进行赋值操作。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的,可靠的,基于字节流传输的通信协议。TCP具有端口号的概念,用来标识同一个地址上的不同应用。
面向流的TCP服务器简单流程图如下图所示。服务器启动后创建服务器Socket,进行相应设置后始终调用accept(2)等待客户端连入。客户端正常连入后,创建一个子进程作为业务进程对特定客户端进行服务,父进程始终作为监听进程等待下一个客户端的连入。
其中为了防止僵尸进程出现,服务器还需要有处理子进程退出的功能,简便起见,程序范例将直接安装一个信号处理程序来处理SIGCHLD信号,此过程因为是完全异步的,并未体现在流程图上。
编写服务器应用程序的流程如下:
①调用 socket()函数打开套接字,得到套接字描述符;
②调用 bind()函数将套接字与IP地址、端口号进行绑定;
③调用listen()函数让服务器进程进入监听状态;
④调用accept()函数获取客户端的连接请求并建立连接;
⑤调用read/recv、write/send与客户端进行通信;
⑥调用close()关闭套接字。
根据上面步骤编写TCP服务器应用程序,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于5000
#define SERVER_IP "192.168.2.136" //服务器IP地址
int main()
{
struct sockaddr_in server_addr = {0};
struct sockaddr_in client_addr = {0};
char ip_str[20] = {0};
int sock_fd, conn_sock;
int addrlen = sizeof(client_addr);
char recvbuf[20];
int ret,i,send_num,recv_num;
char send_buf[20] = "recv from server";
struct timeval timeout;
fd_set sockset; //定义文件描述符集合
sock_fd = socket(AF_INET, SOCK_STREAM, 0); //打开套接字,得到套接字描述符
if (sock_fd < 0)
{
perror("socket error");
exit(1);
}
//将套接字与指定端口号进行绑定
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(SERVER_PORT);
//绑定套接字
if(bind(sock_fd,(struct sockaddr *)&server_addr,sizeof(struct sockaddr_in))<0)//第二个参数需要强转,成功返回0失败返回-1
{
perror("bind error");
close(sock_fd);
exit(1);
}
if(listen(sock_fd, 5)) //服务器进入监听状态,等待用户发起连接请求
{
perror("listen error");
close(sock_fd);
exit(1);
}
conn_sock = accept(sock_fd, (struct sockaddr *)&client_addr, &addrlen); //阻塞等待客户端连接
if (conn_sock < 0)
{
perror("accept error");
close(sock_fd);
exit(1);
}
printf("有客户端接入:");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));//IP地址二进制格式转换成点分十进制字符串
printf("客户端主机IP地址为 %s,", ip_str);
printf("客户端进程的端口号为 %d\n", client_addr.sin_port);
// while(1) //循环接收客户端发送过来的数据并回送
// {
// memset(recvbuf, 0x0, sizeof(recvbuf)); //接收缓冲区清零
// ret = recv(conn_sock, recvbuf, sizeof(recvbuf), 0); //阻塞读数据
// if(ret < 0)
// {
// perror("recv error");
// close(conn_sock);
// break;
// }
// printf("recv success from client: %s\n", recvbuf);//将读取到的数据以字符串形式打印出来
// send_num = send(conn_sock,send_buf,sizeof(send_buf),0); //回送客户端
// if(send_num < 0)
// {
// perror("send error");
// exit(1);
// }
// if (strncmp("exit", recvbuf, 4) == 0) //如果读取到"exit"则关闭套接字退出程序
// {
// printf("server exit...\n");
// close(conn_sock);
// break;
// }
// }
for(i=0;i<10;i++) //循环10次发送数据给客户端并等待客户端回复,调用select()函数等待3s
{
send_num = send(conn_sock,send_buf,sizeof(send_buf),0); //回送客户端
if(send_num < 0)
{
perror("send error");
exit(1);
}
else
{
printf("server send data to client success\n");
FD_ZERO(&sockset); //初始化文件描述符集合sockset为空
FD_SET(conn_sock,&sockset); //添加UDP文件描述符
timeout.tv_sec = 3; //必须每次都要重新赋值,否则下次时间为0不等待就返回
timeout.tv_usec = 0; //不能少,否则一直阻塞
ret = select(conn_sock+1,&sockset,NULL,NULL,&timeout);//阻塞直到文件描述符有数据可读才会返回,timeout设置阻塞时间
if(ret<0) //返回-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(conn_sock,&sockset)) //判断sock_fd文件描述符是否是集合中的成员
{
memset(recvbuf,0,sizeof(recvbuf));
recv_num = recv(conn_sock,recvbuf,sizeof(recvbuf),0);
if(recv_num <0)
{
perror("recvfrom");
exit(1);
}
else
{
printf("recvfrom success:%s\n",recvbuf);
}
}
}
}
}
close(sock_fd); //关闭套接字
return 0;
}
以上我实现了一个简单的TCP服务器应用程序,64-87行实现的功能是当客户端连接到服务器后,67行recv阻塞读取客户端发送的数据,打印出来并回送数据给客户端,直到收到客户端发来的exit字符串则退出程序;89-133行实现的功能则是当客户端连接到服务器后,服务器循环10次发送数据给客户端并等待客户端回复,调用select()函数等待3s,超时进行下一次发送数据。注意后续无论是读还是写数据都是针对accept接收客户端连接返回的新的文件描述符。
程序交叉编译后在开发板上运行作TCP服务端,PC机上运行SocketTool工具新建TCP客户端,PC机和开发板通过网线直连并设置在同一号段。客户端设置要连接的服务端IP和端口如下图所示,对应程序中填充的sockaddr_in结构。
首先测试接收客户端数据并回送功能,开发板运行程序,然后在SocketTool客户端点击“连接”,开发板控制台终端就会打印有客户机连接,随后在SocketTool客户端发送数据,可以在控制台上看到数据接收成功,完整的测试过程如下:
TCP服务端socket编程测试结果
最后测试服务端率先发送数据到客户端并调用select函数等待客户端回送数据。在SocketTool窗口接收到来自开发板服务端数据,若在3s内回送数据则服务端能够接收,否则超时服务端不再等待。完整的测试过程如下:
TCP服务端socket编程测试结果2
对于客户机程序,处理流程见下图。启动后立即创建Socket,并且直接调用connect(2)连接服务器,省去bind(2)调用,系统将会将刚才创建的Socket隐式绑定到一个随机端口上。connect(2)后直接发送数据到服务器,发送完毕有直接读取服务器回发的数据并打印接收到的数据后结束。
具体实现代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.2.100" //服务器的 IP 地址
int main()
{
struct sockaddr_in server_addr = {0};
char buf[20];
int sock_fd;
int ret,i,recv_num;
fd_set sockset; //定义文件描述符集合
struct timeval timeout;
sock_fd = socket(AF_INET, SOCK_STREAM, 0); //打开套接字,得到套接字描述符
if (sock_fd < 0)
{
perror("socket error");
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);//也可以这样填充IP地址
// inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP地址,将字符串转换为二进制,第三个参数必须是struct in_addr对象
ret = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)); //调用connect(阻塞)连接远端服务器
if (ret < 0)
{
perror("connect error");
close(sock_fd);
exit(1);
}
printf("服务器连接成功...\n\n");
for(i=0;i<10;i++) //向服务器发送数据
{
memset(buf, 0, sizeof(buf)); //清理缓冲区
strcpy(buf,"hello,yrr!");
ret = send(sock_fd, buf, strlen(buf), 0); //将输入的数据发送给服务器
if(ret < 0)
{
perror("send error");
break;
}
else
{
printf("send success:%s ,waitting for server ack\n",buf);
FD_ZERO(&sockset); //初始化文件描述符集合sockset为空
FD_SET(sock_fd,&sockset); //添加UDP文件描述符
timeout.tv_sec = 3; //必须每次都要重新赋值,否则下次时间为0不等待就返回
timeout.tv_usec = 0; //不能少,否则一直阻塞
ret = select(sock_fd+1,&sockset,NULL,NULL,&timeout);//阻塞直到文件描述符有数据可读才会返回,timeout设置阻塞时间
if(ret<0) //返回-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(buf,0,sizeof(buf));
recv_num = read(sock_fd,buf,sizeof(buf));
if(recv_num <0)
{
perror("read");
exit(1);
}
else
{
printf("read success from client:%s\n",buf);
}
}
}
}
}
close(sock_fd);
return 0;
}
程序交叉编译后在开发板上运行作TCP客户端,PC机上运行TCP_tester工具新建TCP服务端,设置端口为8888,PC机和开发板通过网线直连并设置在同一号段。点击“开始侦听”,开发板上运行程序就可以连接到服务器,然后就可以实现收发数据了。测试的完整过程如下:
TCP客户端socket编程测试结果
Wireshark是非常流行的网络封包分析软件,简称小鲨鱼,功能十分强大。可以截取各种网络封包,显示网络封包的详细信息。Wireshark是开源软件,可以放心使用。可以运行在Windows和Mac OS上。对应的,linux下的抓包工具是tcpdump。使用Wireshark的人必须了解网络协议,否则就看不懂Wireshark了。
只要涉及到网络通信,尤其是调试过程中遇见网络不通或是数据异常就需要借助Wireshark工具抓包分析了,WireShark是非常流行的网络封包分析工具,可以截取各种网络数据包,并显示数据包详细信息。常用于开发测试过程中各种问题定位。
Wireshark软件打开后主界面如下图所示,选择对应的网卡,右键会出现Start Capture(开始捕获),点击即可进行捕获该网络信息,开始抓取网络包;也可点击上方工具栏“鲨鱼”形按键快速开始捕获。
双击“WLAN”对无限网卡上的网络数据进行抓包,进入抓包界面,如下图所示:
主要是以下几个模块:
显示过滤器:用于设置过滤条件进行数据包列表过滤。
数据包列表:显示捕获到的数据包,每个数据包包含编号(No)、时间戳(Time)、源地址(Source)、目标地址(Destination)、协议(Protocol)、长度(Length)、数据包信息(Info)。不通协议的数据包使用了不同的颜色区分显示。协议颜色标识定位在菜单栏视图→着色规则。
数据包详细信息:在数据包列表中选择指定数据包,在数据包详细信息中会显示该数据包的所有详细内容。数据包详细信息面板是最重要的,用来查看协议中的每一个字段。不同的协议数据信息列表是不一样的,UDP显示的各行信息是如下图所示这样的:
而TCP如下图,主要区别在于传输层协议不同:
①Frame:物理层的数据帧概况,具体信息如下图所示:
②Ethernet II:数据链路层以太网帧头部信息,具体信息如下图所示:
③Internet Protocol Version 4:互联网层IP包头部信息,具体信息如下图所示:
④User Datagram Protocol / Transmission Control Protocol:传输层数据段头部信息,分为TCP协议和UDP协议,如下图所示:
⑤data:应用层数据报文信息,对于其他协议有不同的名称,例如HTTP协议是HypertextTransfer Protocol。
数据包字节区:报文原始内容,界面如下图:
使用Wireshark可能会得到大量的冗余数据包列表,以至于很难找到自己需要抓取的数据包部分。Wireshark工具中自带了两种类型的过滤器,抓包过滤器和显示过滤器,一定要区分两者概念。
捕获过滤器的菜单栏路径为捕获→捕获过滤器。用于在抓取数据包前设置。
可点击“+”添加新的抓包过滤条件,默认是捕获所有符合上述条件的网络数据包,可以添加过滤条件,如ip host 192.168.2.136表示只捕获主机IP为192.168.2.136的数据包。
显示过滤器是用于在抓取数据包后设置过滤条件进行过滤数据包。通常是在抓取数据包时设置条件相对宽泛或者没有设置导致抓取的数据包内容较多时使用显示过滤器设置条件过滤以方便分析。显示过滤器就在主界面上方,如下图:
以直接通过无线网卡捕获所有数据包为例,未设置抓包过滤规则,数据包列表中含有大量的无效数据。这时就可以通过设置显示过滤器条件进行提取分析信息。例如输入ip.addr == 192.168.2.136进行过滤就可以滤除掉大量我们不想看到的数据信息。
(1)协议过滤,比较简单,直接在抓包过滤框中直接输入协议名即可,如下所示:
①tcp,只显示TCP协议的数据包列表
②http,只查看HTTP协议的数据包列表
③icmp,只显示ICMP协议的数据包列表
(2)IP过滤
①host 192.168.1.104
②src host 192.168.1.104
③dst host 192.168.1.104
(3)端口过滤
①port 80
②src port 80
③dst port 80
(4)逻辑运算符与&&、或|| 、非!
①src host 192.168.1.104 && dst port 80:抓取主机地址为192.168.1.80、目的端口为80的数据包
②host 192.168.1.104 || host 192.168.1.102:抓取主机为192.168.1.104或者192.168.1.102的数据包
③! broadcast:不抓取广播数据包
(1)比较操作符有:==等于、!= 不等于、>大于、<小于、>=大于等于、<=小于等于。
(2)协议过滤:比较简单,直接在Filter框中直接输入协议名即可。注意协议名称需要输入小写。tcp,只显示TCP协议的数据包列表;http,只查看HTTP协议的数据包列表;icmp,只显示ICMP协议的数据包列表。
(3)ip过滤:
ip.src ==222.134.133.110:显示源地址为222.134.133.110的数据包列表;
ip.dst == 222.134.133.110: 显示目标地址为222.134.133.110的数据包列表;
ip.addr == 222.134.133.110:显示源IP地址或目标IP地址为222.134.133.110的数据包列表。
(4)端口过滤:
tcp.port ==80:显示源主机或者目的主机端口为80的数据包列表。
tcp.srcport == 80:只显示TCP协议的源主机端口为80的数据包列表。
tcp.dstport == 80:只显示TCP协议的目的主机端口为80的数据包列表。
(5)逻辑运算符为 and/or/not:过滤多个条件组合时,使用and/or。比如获取IP地址为192.168.2.136的ICMP数据包表达式为ip.addr == 192.168.2.136 and icmp。
以上面的TCP客户端测试作为实战背景,在PC机上使用Wireshark进行抓包。开发板上运行TCP客户端发送程序,PC机上TCP_tester工具作为服务端接收数据并回送。对PC机以太网进行抓包,设置显示过滤条件为ip.addr == 192.168.2.136。
首先开发板上运行客户端程序,服务端还没开始侦听,如下图所示可以看到开发板一直在发送请求建立连接报文,也就是第一次握手数据包,协议为TCP,标志位为[SYN](表示请求建立连接),序列号Seq=0(从0开始表示还没有发送数据),Ack=0(表示已经收到包的数量,0表示当前没有接收到数据)。
服务端点击“开始侦听”,TCP建立连接,可以看到三次握手报文,如下图所示:
编号为7的数据包就是二次握手报文,标志位为[SYN,ACK]表示同意建立连接,确认序号Ack+1=1,seq=0表示还没有发送数据。编号为8的数据包就是三次握手报文,标志位为[ACK]表示已经收到,Seq=1表示当前已发送1个数据,Ack=1表示当前端成功收到的数据位数。
随后就可以进行数据交互了,点击一个客户端发送的数据包,可以看到发送的具体内容,如下图所示:
过滤的数据包列表中可以看到只有ip为192.168.2.136的网络包,还可以在Frame信息中看到数据包发送的具体时刻。
tcpdump是Linux系统下的一个强大的命令,可以将网络中传送的数据包完全截获下来提供分析。它支持针对网络层、协议、主机、网络或端口的过滤,并提供and、or、not等逻辑语句来帮助你去掉无用的信息。
tcpdump命令可以搭配很多种参数,本文不一一列举,网上有很多可供查阅的资料。下面介绍一些常用的命令示例初步了解tcpdump的使用。
①抓取地址包含是192.168.2.136的包,并将结果保存到test.cap文件中的命令是:
tcpdump host 192.168.2.136
tcpdump host 192.168.2.136 -w result.cap
②抓取指定协议格式的数据包,协议格式可以是udp、icmp、arp、ip中的任何一种,例如以下命令:
tcpdump udp -i eth0 -vnn
③抓取本机端口是22的数据包:
tcpdump port 22
tcpdump port 22 -w test.cap
如果需要详细查看报文情况,用tcpdump port 30080 -w file.cap命令,运行后等待将捕获的数据包详细信息写入文件中,此时并不打印出来,ctrl + C结束抓包后可用Wireshark软件打开分析。下面演示在运行Linux操作系统的开发板上使用tcpdump命令进行网络抓包。
PC机用网线连接开发板eth1网口,开发板上运行tcpdump -i eth1命令指定对eth1网卡抓包(如果不指定网卡,默认tcpdump只会监视第一个网络接口,一般是eth0)。在电脑上ping开发板,可以看到控制台终端打印捕获的网络包信息,如下图所示:
更多【wireshark-网络socket编程(二)——面向流的TCP编程及测试(SocketTool)、Wireshark软件使用】相关视频教程:www.yxfzedu.com