前言
在这里,我们是把网络设备当成输入设备,由于使用的是套接字网络编程,套接字编程通常分为客户端(Client)和服务器(Server端)两部分,在这里应用程序的主体作为Server端,另写一个简单的Client端配合进行测试。
主用到的网络设备Server端的功能,所以没有用到Client的功能,所以只需写其作为Server端的代码即可。
完整源代码
Server端代码
Server端整个代码都在文件09_input_netinput_unittest\input\netinput.c
中,如下:
#include <input_manager.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/* socket
* bind
* sendto/recvfrom
*/
#define SERVER_PORT 8888
static int g_iSocketServer;
static int NetinputGetInputEvent(PInputEvent ptInputEvent)
{
struct sockaddr_in tSocketClientAddr;
int iRecvLen;
char aRecvBuf[1000];
unsigned int iAddrLen = sizeof(struct sockaddr);
iRecvLen = recvfrom(g_iSocketServer, aRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (iRecvLen > 0)
{
aRecvBuf[iRecvLen] = '\0';
//printf("Get Msg From %s : %s\n", inet_ntoa(tSocketClientAddr.sin_addr), ucRecvBuf);
ptInputEvent->iType = INPUT_TYPE_NET;
gettimeofday(&ptInputEvent->tTime, NULL);
strncpy(ptInputEvent->str, aRecvBuf, 1000);
ptInputEvent->str[999] = '\0';
return 0;
}
else
return -1;
}
static int NetinputDeviceInit(void)
{
struct sockaddr_in tSocketServerAddr;
int iRet;
g_iSocketServer = socket(AF_INET, SOCK_DGRAM, 0);
if (-1 == g_iSocketServer)
{
printf("socket error!\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = bind(g_iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
return 0;
}
static int NetinputDeviceExit(void)
{
close(g_iSocketServer);
return 0;
}
static InputDevice g_tNetinputDev ={
.name = "touchscreen",
.GetInputEvent = NetinputGetInputEvent,
.DeviceInit = NetinputDeviceInit,
.DeviceExit = NetinputDeviceExit,
};
#if 1
int main(int argc, char **argv)
{
InputEvent event;
int ret;
g_tNetinputDev.DeviceInit();
while (1)
{
ret = g_tNetinputDev.GetInputEvent(&event);
if (ret) {
printf("GetInputEvent err!\n");
return -1;
}
else
{
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
}
return 0;
}
#endif
Client端的代码
Client端的代码在文件\unittest\client.c
中,代码比较简单:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
/* socket
* connect
* send/recv
*/
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
int iSendLen;
int iAddrLen;
if (argc != 3)
{
printf("Usage:\n");
printf("%s <server_ip> <str>\n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_DGRAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
#if 0
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
#endif
iAddrLen = sizeof(struct sockaddr);
iSendLen = sendto(iSocketClient, argv[2], strlen(argv[2]), 0,
(const struct sockaddr *)&tSocketServerAddr, iAddrLen);
close(iSocketClient);
return 0;
}
头文件include\input_manager.h
这个工程用到的头文件和嵌入式应用实例→电子产品量产工具→触摸屏输入系统
的头文件是相同的,所以这个工程的头文件的分析就略过,详情见 https://blog.youkuaiyun.com/wenhao_ir/article/details/144609033
服务端的C文件input\netinput.c
的分析
函数 NetinputDeviceInit()
函数 NetinputDeviceInit()
用于对网络设备进行初始化。
在阅读这个函数的代码前建议把我写的另一篇博文:
Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
认真读一遍,认真读一遍之后看下面的代码就简单了。
static int NetinputDeviceInit(void)
{
struct sockaddr_in tSocketServerAddr;
int iRet;
g_iSocketServer = socket(AF_INET, SOCK_DGRAM, 0); //SOCK_DGRAM代表使用UDP网络协议
if (-1 == g_iSocketServer)
{
printf("socket error!\n");
return -1;
}
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
memset(tSocketServerAddr.sin_zero, 0, 8);
iRet = bind(g_iSocketServer, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("bind error!\n");
return -1;
}
return 0;
}
下面这句代码:
struct sockaddr_in tSocketServerAddr;
定义了一个类型为sockaddr_in
的结构体。关于sockaddr_in
的详细介绍见我的另一篇博文:https://blog.youkuaiyun.com/wenhao_ir/article/details/144660421
为什么不需要进入监听状态?
我的疑问:
为什么没有像博文:
Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
提供的例子中,调用函数listen()
使服务端的套接字进入监听状态?难道不进入监听状态也可以收到数据?
答:这个问题问的好,因为这里用的是UDP协议,它是一种无连接的协议,不需要临听也可以收数据的。
函数NetinputGetInputEvent()
的分析
说明:在阅读这个函数的代码前同样建议把我写的另一篇博文:
Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
认真读一遍,认真读一遍之后看下面的代码就更容易理解了。
源代码:
static int NetinputGetInputEvent(PInputEvent ptInputEvent)
{
struct sockaddr_in tSocketClientAddr;
int iRecvLen;
char aRecvBuf[1000];
unsigned int iAddrLen = sizeof(struct sockaddr);
iRecvLen = recvfrom(g_iSocketServer, aRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
if (iRecvLen > 0)
{
aRecvBuf[iRecvLen] = '\0';
//printf("Get Msg From %s : %s\n", inet_ntoa(tSocketClientAddr.sin_addr), ucRecvBuf);
ptInputEvent->iType = INPUT_TYPE_NET;
gettimeofday(&ptInputEvent->tTime, NULL);
strncpy(ptInputEvent->str, aRecvBuf, 1000);
ptInputEvent->str[999] = '\0';
return 0;
}
else
return -1;
}
代码iRecvLen = recvfrom(g_iSocketServer, aRecvBuf, 999, 0, (struct sockaddr *)&tSocketClientAddr, &iAddrLen);
的理解
这句代码主要就是第三个参数999和第四个参数0可能不知道是什么意思。
关于第三个参数999的理解见我的另一篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144669577?
第四个参数0的解释如下:
这是 recvfrom() 函数的 标志位(flags),它控制接收数据的方式。该参数通常用于指定不同的接收行为。0 表示使用默认行为,即不应用任何特殊的接收标志。如果你需要特定的行为(例如接收更多数据,或者想要暂时不删除缓冲区中的数据),可以设置不同的标志,如 MSG_WAITALL、MSG_PEEK 等。
代码gettimeofday(&ptInputEvent->tTime, NULL);
这行代码:
gettimeofday(&ptInputEvent->tTime, NULL);
调用了 gettimeofday
函数,用于获取当前的系统时间,并将其存储在 ptInputEvent->tTime
中。我们可以逐步理解:
gettimeofday
是一个获取当前时间的标准库函数,它的原型是:
int gettimeofday(struct timeval *tv, struct timezone *tz);
tv
:这是一个指向struct timeval
类型的指针,用来存储返回的当前时间。tz
:这是一个指向struct timezone
类型的指针,表示时区信息。通常情况下,可以将其设置为NULL
,表示不需要时区信息(即不关心时区偏差)。
struct timeval
结构体定义如下:
struct timeval {
time_t tv_sec; /* 秒数 */
suseconds_t tv_usec; /* 微秒数 */
};
tv_sec
:自1970年1月1日(即Unix纪元)以来的秒数。tv_usec
:当前秒内的微秒数(范围为0到999,999微秒)。
代码解释:
gettimeofday(&ptInputEvent->tTime, NULL)
获取当前的系统时间(秒和微秒),并将其存储在ptInputEvent
指向的InputEvent
结构体中的tTime
字段里。这里tTime
是一个struct timeval
类型。NULL
作为第二个参数,表示不关心时区信息。
作用:
这行代码的作用是将当前的系统时间(精确到微秒)记录到 ptInputEvent->tTime
中,通常用于时间戳记录,方便后续进行时间比较、日志输出或其他需要时间信息的操作。
代码ptInputEvent->str[999] = '\0';
的分析
我对函数NetinputGetInputEvent()的下面这句代码有疑问:
ptInputEvent->str[999] = '\0';
分两种情况:
第1种:
假如收到的数据为最大长度999,那么通过语句aRecvBuf[iRecvLen] = ‘\0’;已经把第1000个的值置为’\0’了,为什么还要作一次把第1000个的值置为’\0’的操作?
第2种:
假如收到的数据最大长度没有超过999,比如只有99个,那么此时把第1000个的值置为’\0’有何意义?
答:
在阅读下面的回答之前,可以先看下博文:
C语言的strncpy()函数在什么情况下会添加字符串末尾符‘\0‘,又什么情况下不会添加?
让我们分别分析您提出的两种情况:
第1种情况:收到的数据长度为最大值 999
在这种情况下:
recvfrom()
接收到的数据填充了aRecvBuf
的前 999 个字节。- 紧接着,代码通过
aRecvBuf[iRecvLen] = '\0';
将第 1000 个位置(aRecvBuf[999]
)置为'\0'
,将接收到的字节流终结为一个以'\0'
结尾的 C 字符串。 - 然后,代码执行:
这里的ptInputEvent->str[999] = '\0';
ptInputEvent->str
是另一个字符串缓冲区,大小同样为 1000 字节。通过strncpy()
将aRecvBuf
的内容复制到ptInputEvent->str
中。
此时的关键在于:
- 虽然
aRecvBuf
已经正确终结,但strncpy()
的行为不会自动为目标缓冲区(ptInputEvent->str
)添加'\0'
,如果aRecvBuf
的内容正好是 999 个字符,strncpy()
会将目标字符串填满,但可能无法添加终结符。 - 因此,为了确保
ptInputEvent->str
无论何种情况都以'\0'
终结,额外执行ptInputEvent->str[999] = '\0';
是一种防御性编程手段。
第2种情况:收到的数据长度小于 999
(比如只有 99
个字节)
在这种情况下:
recvfrom()
接收到 99 个字节,将它们填充到aRecvBuf
的前 99 个位置。- 接着,通过
aRecvBuf[iRecvLen] = '\0';
将第 100 个位置(aRecvBuf[99]
)置为'\0'
,完成字符串终结。 - 然后,
strncpy()
将aRecvBuf
的内容复制到ptInputEvent->str
中,这时目标缓冲区的第 100 个位置(ptInputEvent->str[99]
)是'\0'
。 - 接下来,代码执行
ptInputEvent->str[999] = '\0';
,尽管前 100 个字节已经正确设置,但额外将第 1000 个字节(ptInputEvent->str[999]
)置为'\0'
。
此时的行为:
- 在这种情况下,设置
ptInputEvent->str[999] = '\0';
实际上没有实际意义,因为之前strncpy()
已经正确处理了拷贝的部分内容,且字符串的第一个终结符('\0'
)已经足够满足需求。 - 这通常是编程习惯的一部分,用于统一逻辑,确保无论任何情况,
ptInputEvent->str
的最后一个字节始终是'\0'
,即便这个位置没有被实际使用。
为什么还要额外设置 ptInputEvent->str[999] = '\0';
?
总结两种情况,这样设计的原因主要是:
-
安全性和鲁棒性:
- 无论数据长度是多少,这一行代码都确保目标字符串缓冲区
ptInputEvent->str
的最后一个字节(第 1000 个位置)始终是'\0'
。 - 如果程序的其他部分无意中读取了缓冲区的第 1000 个字节(尽管它不属于有效数据),仍然会看到一个安全的终结符,而不是随机垃圾数据。
- 无论数据长度是多少,这一行代码都确保目标字符串缓冲区
-
防御性编程:
- 防止未来修改程序时,
strncpy()
的行为或缓冲区的其他使用逻辑导致潜在问题。
- 防止未来修改程序时,
-
无害性:
- 即便数据不足
999
字节,这一操作不会产生任何副作用,因为设置缓冲区末尾的额外字节并不影响之前的数据内容。
- 即便数据不足
改进建议
如果明确知道 strncpy()
的行为以及上下文不需要多余的防御性措施,可以省略这行代码。但对于复杂嵌入式系统而言,这种冗余操作虽然看似多余,却是一种确保健壮性的小技巧。
主函数代码分析
主函数代码
int main(int argc, char **argv)
{
InputEvent event;
int ret;
g_tNetinputDev.DeviceInit();
while (1)
{
ret = g_tNetinputDev.GetInputEvent(&event);
if (ret) {
printf("GetInputEvent err!\n");
return -1;
}
else
{
printf("Type : %d\n", event.iType);
printf("str : %s\n", event.str);
}
}
return 0;
}
结构体InputEvent
这个结构体是在头文件include\input_manager.h
中定义的,如下:
typedef struct InputEvent {
struct timeval tTime;
int iType;
int iX;
int iY;
int iPressure;
char str[1024];
}InputEvent, *PInputEvent;
主函数后面的代码没什么好说的,只要读懂了函数NetinputGetInputEvent()
就很简单了。
客户端的C文件unittest\client.c
的分析
客户端的源代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
/* socket
* connect
* send/recv
*/
#define SERVER_PORT 8888
int main(int argc, char **argv)
{
int iSocketClient;
struct sockaddr_in tSocketServerAddr;
int iRet;
int iSendLen;
int iAddrLen;
if (argc != 3)
{
printf("Usage:\n");
printf("%s <server_ip> <str>\n", argv[0]);
return -1;
}
iSocketClient = socket(AF_INET, SOCK_DGRAM, 0);
tSocketServerAddr.sin_family = AF_INET;
tSocketServerAddr.sin_port = htons(SERVER_PORT); /* host to net, short */
//tSocketServerAddr.sin_addr.s_addr = INADDR_ANY;
if (0 == inet_aton(argv[1], &tSocketServerAddr.sin_addr))
{
printf("invalid server_ip\n");
return -1;
}
memset(tSocketServerAddr.sin_zero, 0, 8);
#if 0
iRet = connect(iSocketClient, (const struct sockaddr *)&tSocketServerAddr, sizeof(struct sockaddr));
if (-1 == iRet)
{
printf("connect error!\n");
return -1;
}
#endif
iAddrLen = sizeof(struct sockaddr);
iSendLen = sendto(iSocketClient, argv[2], strlen(argv[2]), 0,
(const struct sockaddr *)&tSocketServerAddr, iAddrLen);
close(iSocketClient);
return 0;
}
可见,只有一个主函数,我们就来分析这个主函数,注意,这次的分析默认你是读了上面的服务端的分析内容,所以有些上面出现的知识点或代码这里就不再赘述了。
另外,强烈建议看这里之前去看下:
Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
代码段 if (argc != 3)...
的分析
if (argc != 3)
{
printf("Usage:\n");
printf("%s <server_ip> <str>\n", argv[0]);
return -1;
}
如果 argc
的值不为 3,那么代码会输出以下内容:
Usage:
<程序名称> <server_ip> <str>
其中 <程序名称>
是程序被执行时的名称,通常是运行该程序时输入的文件名。例如,如果执行命令为 ./udp_client 192.168.1.1 message
,那么 <程序名称>
就是 ./udp_client
。
假设程序名为 udp_client
,则可能输出:
Usage:
./udp_client <server_ip> <str>
解释:
argc
表示命令行参数的数量,包含程序名本身。- 如果
argc != 3
,说明参数数量不正确,程序打印用法信息,并退出。
具体在这里,第一个参数是程序名,第二个参数是服务器的IP(即你想把数据发到哪个IP地址上),第三个参数是要发送的字符串。
代码inet_aton(argv[1], &tSocketServerAddr.sin_addr)
要注意与博文 :
Linux嵌入式系统利用套接字编程(Socket Programming)实现网络通信的基础知识并附对一个简单实例的分析
中给的客户端的示例代码相区分,在上面这篇博文中,相关代码如下:
inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
而这里是:
inet_aton(argv[1], &tSocketServerAddr.sin_addr)
可见,前者是pton,后者是aton,inet_aton
和 inet_pton
功能相似,都用于将命令行参数中的字符串IP地址转换为网络字节序的二进制地址,适配于 sockaddr_in 的 sin_addr 字段,但用法有一些不同。下面对比一下两者:
inet_aton
- 用于处理 IPv4 地址。
- 原型:
int inet_aton(const char *cp, struct in_addr *inp);
- 特点:
- 只能处理IPv4地址,无法处理IPv6。
- 将字符串形式的IPv4地址(如
"192.168.1.1"
)转换为struct in_addr
类型。
- 示例:
struct in_addr addr; if (inet_aton("192.168.1.1", &addr)) { printf("Conversion successful\n"); } else { printf("Invalid IP address\n"); }
inet_pton
- 用于处理 IPv4 和 IPv6 地址。
- 原型:
int inet_pton(int af, const char *src, void *dst);
- 参数:
af
:地址族,通常为AF_INET
或AF_INET6
。src
:字符串形式的IP地址。dst
:存储转换结果的指针(对于IPv4,是struct in_addr *
;对于IPv6,是struct in6_addr *
)。
- 特点:
- 更通用,支持IPv4和IPv6。
- 如果
af
为AF_INET
,功能类似于inet_aton
。
- 示例:
struct in_addr addr; if (inet_pton(AF_INET, "192.168.1.1", &addr) == 1) { printf("Conversion successful\n"); } else { printf("Invalid IP address\n"); }
- 二者的区别总结
- 地址支持:
inet_aton
仅支持IPv4,inet_pton
支持IPv4和IPv6。 - 参数结构:
inet_aton
不需要指定地址族,直接操作IPv4地址。inet_pton
需要指定地址族,适配IPv4和IPv6。
- 兼容性:
inet_aton
是较旧的API,可能在某些环境中不被支持。inet_pton
是更现代、更通用的API。
如果需要兼容IPv6,建议使用 inet_pton
。如果仅处理IPv4,inet_aton
或 inet_pton
都可以使用。
后面的代码就没有啥好说的了。
代码iSendLen = sendto(iSocketClient, argv[2], strlen(argv[2]), 0, (const struct sockaddr *)&tSocketServerAddr, iAddrLen);
这行代码:
iSendLen = sendto(iSocketClient, argv[2], strlen(argv[2]), 0, (const struct sockaddr *)&tSocketServerAddr, iAddrLen);
调用了 sendto
函数,用于向指定的目标地址发送数据。我们可以逐步分析每个参数的作用:
sendto
函数原型:
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd
:iSocketClient
,这是一个已打开的套接字描述符,代表客户端套接字,数据会通过这个套接字发送。buf
:argv[2]
,这是一个指向数据的指针,表示要发送的数据内容。在这个代码中,argv[2]
是程序运行时传入的第二个参数,即一个字符串。len
:strlen(argv[2])
,这是要发送的数据的长度。在这个代码中,strlen(argv[2])
返回argv[2]
字符串的长度(即字符串中的字符数,不包括末尾的空字符\0
)。flags
:0
,这是发送数据时的一些标志参数,通常为0,表示没有特别的发送选项。dest_addr
:(const struct sockaddr *)&tSocketServerAddr
,这是目标地址(服务器地址)的指针。在这个代码中,tSocketServerAddr
是已经配置好的服务器地址结构体,它包含了服务器的IP地址和端口号。addrlen
:iAddrLen
,这是目标地址结构的长度。在这个代码中,iAddrLen
的值是sizeof(struct sockaddr)
,即目标地址的大小。
sendto
的作用:
sendto
函数用于将数据从客户端发送到指定的服务器地址。在这行代码中,客户端向由 tSocketServerAddr
定义的服务器发送一个字符串(argv[2]
),并传递该字符串的长度(strlen(argv[2])
)。由于使用的是UDP(无连接),因此不需要建立连接,直接将数据包发送到目标地址。
返回值:
iSendLen
存储了sendto
函数的返回值,这个返回值表示成功发送的字节数。如果成功发送,返回值等于len
(即字符串的长度);如果发生错误,返回值会是-1
,并且可以通过errno
获取具体的错误原因。
总结:
这行代码的目的是通过UDP协议将命令行参数中的第二个字符串(argv[2]
)发送到指定的服务器地址(argv[1]
指定的IP和固定的端口8888
)。
交叉编译生成可执行程序
对网络套接字通信的服务器端程序进行交叉编译生成可执行程序
服务端修改Makefile文件进行编译,修改的方法不再赘述,请参考下面两篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144532544
https://blog.youkuaiyun.com/wenhao_ir/article/details/144532544
顶层的Makfile:
子目录中的Makefile
工程复制到Ubuntu中的目录/home/book/mycode
下,并重命名为:C0007_input_netinput_unittest
进入目录C0007_input_netinput_unittest
,然后make
cd /home/book/mycode/C0007_input_netinput_unittest
make
把生成的test可执行文件重命名为net_server_test,然后复制到nfs文件下,以备待用
对网络套接字通信的客户端程序进行交叉编译生成可执行程序
客户端的程序我们进行手动编译,因为它其实只有一个C文件,命令如下:
cd /home/book/mycode/C0007_input_netinput_unittest/unittest
arm-buildroot-linux-gnueabihf-gcc -o net_client_test client.c
生成之后复制到NFS目录中,待用:
上板测试
打开开发板,挂载网络文件系统:
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
为两个文件添加可执行权限:
cd /mnt
chmod +x net_server_test
chmod +x net_client_test
然后首先启动服务端,并且让其在后台运行:
./net_server_test &
然后执行客户端程序,注意有参数:
./net_client_test 127.0.0.1 "my name is SuWenhao"
这样就是测试成功了。