目录
1. Socket 概述
Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。
套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。
总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口,也是在Internet上进行应用开发最为通用的API。
为了能让更多开发者直接上手LwIP 的编程,专门设计了LwIP 的第三种编程接口——Socket API,它兼容BSD Socket。
Socket 虽然是能在多平台移植,但是LwIP 中的Socket 并不完善,因为LwIP 设计之初就是为了在嵌入式平台中使用,它只实现了完整Socket 的部分功能,不过,在嵌入式平台中,这些功能早已足够。
2. LwIP 中的socket
在LwIP 中,Socket API 是基于NETCONN API 之上来实现的,系统最多提供MEMP_NUM_NETCONN 个netconn 连接结构,因此决定Socket 套接字的个数也是那么多个。
为了更好对netconn 进行封装,LwIP 还定义了一个套接字结构体——lwip_sock(称之为Socket 连接结构),每个lwip_sock 内部都有一个netconn 的指针,实现了对netconn 的再次封装。
LwIP 定义了一个lwip_sock 类型的sockets数组,通过套接字就可以直接索引并且访问这个结构体了,这也是为什么套接字是一个整数的原因,lwip_sock 结构体是比较简单的,因为基本上全是依赖netconn 实现。
#define NUM_SOCKETS MEMP_NUM_NETCONN // 默认是4
/** 全局可用套接字数组 **/
static struct lwip_sock sockets[NUM_SOCKETS];
union lwip_sock_lastdata {
struct netbuf *netbuf;
struct pbuf *pbuf;
};
/** 包含用于套接字的所有内部指针和状态*/
struct lwip_sock {
/** 套接字当前是在netconn 上构建的,每个套接字都有一个netconn*/
struct netconn *conn;
/** 从上一次读取中留下的数据 */
union lwip_sock_lastdata lastdata;
#if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL
/** number of times data was received, set by event_callback(),
tested by the receive and select functions */
s16_t rcvevent;
/** number of times data was ACKed (free send buffer), set by event_callback(),
tested by select */
u16_t sendevent;
/** error happened for this socket, set by event_callback(), tested by select */
u16_t errevent;
/** 使用select 等待此套接字的线程数 */
SELWAIT_T select_waiting;
#endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */
#if LWIP_NETCONN_FULLDUPLEX
/* counter of how many threads are using a struct lwip_sock (not the 'int') */
u8_t fd_used;
/* status of pending close/delete actions */
u8_t fd_free_pending;
#define LWIP_SOCK_FD_FREE_TCP 1
#define LWIP_SOCK_FD_FREE_FREE 2
#endif
};
3. Socket API
3.1 socket()
向内核申请一个套接字,在本质上该函数其实就是对netconn_new()函数进行了封装,虽然说不是直接调用它,但是主体完成的工作就做了 netconn_new()函数的事情,而且该函数本质是一个宏定义.
/** @ingroup socket */
#define socket(domain,type,protocol) lwip_socket(domain,type,protocol)
int
lwip_socket(int domain, int type, int protocol);
#define AF_INET 2
/* Socket protocol types (TCP/UDP/RAW) */
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3
参数domain :表示该套接字使用的协议簇,对于TCP/IP 协议来说,该值始终为AF_INET。
参数type: 指定了套接字使用的服务类型,可能的类型有3 种:
1. SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的Socket 服务,多用于资料(如文件)传输,如TCP 协议。
2. SOCK_DGRAM:是提供无保障的面向消息的Socket 服务,主要用于在网络上发广播信息,如UDP 协议,提供无连接不可靠的数据报交付服务。
3. SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。
参数protocol: 指定了套接字使用的协议,在IPv4 中,只有TCP 协议提供SOCK_STREAM这种可靠的服务,只有UDP 协议供SOCK_DGRAM服务,对于这两种协议,protocol 的值均为0。
当申请套接字成功的时候,该函数返回一个int 类型的值,也是Socket 描述符,用户通过这个值可以索引到一个Socket 连接结构——lwip_sock,当申请套接字失败时,该函数返回-1。
3.2 bind()
该函数的功能与netconn_bind()函数是一样的,用于服务器端绑定套接字与网卡信息,实际上就是对netconn_bind()函数进行了封装,可以将一个申请成功的套接字与网卡信息进行绑定。
/** @ingroup socket */
#define bind(s,name,namelen) lwip_bind(s,name,namelen)
int
lwip_bind(int s, const struct sockaddr *name, socklen_t namelen);
参数s : 表示要绑定的Socket 套接字
参数name: 是一个指向sockaddr 结构体的指针,其中包含了网卡的IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用bind()的时候,只需要直接填写相关字段即可.
参数namelen: 指定了name 结构体的长度
struct sockaddr {
u8_t sa_len; /* 长度 */
sa_family_t sa_family; /* 协议簇 */
char sa_data[14]; /* 连续的14字节信息 */
};
需要填写的IP 地址与端口号等信息,都在sa_data 连续的14 字节信息里面,但是这个数据对我们不友好,因此LwIP 还定义了另一个对开发者更加友好的结构体——sockaddr_in,我们一般也是用这个结构体.
/* members are in network byte order */
struct sockaddr_in {
u8_t sin_len; // 长度
sa_family_t sin_family; // 协议簇 uint8_t
in_port_t sin_port; // 端口 uint16_t
struct in_addr sin_addr; // 地址 uint32_t
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN];
};
这个结构体的前两个字段是与sockaddr 结构体的前两个字段一致,而剩下的字段就是sa_data 连续的14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr 字段是我们需要填写的IP 地址信息,剩下sin_zero区域的8 字节保留未用.
使用例程:
#define PORT 5001
#define IP_ADDR "192.168.0.181"
int sock = -1;
struct sockaddr_in server_addr;
sock = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (bind(sock, (struct sockaddr *)&server_addr,
sizeof(struct sockaddr)) == -1) {
;
}
3.3 connect()
函数的作用与netconn_connect()函数的作用基本一致,因为就是封装了netconn_connect()函数。它用于客户端中,将Socket 与远端IP 地址、端口号进行绑定,在TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个TCP 连接请求),并最终建立新的TCP 连接,而对于UDP 协议来说,调用这个函数只是在UDP 控制块中记录远端IP 地址与端口号,而不发送任何数据,参数信息与bind()函数是一样的.
/** @ingroup socket */
#define connect(s,name,namelen) lwip_connect(s,name,namelen)
int
lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);
3.4 listen()
函数是对netconn_listen()函数的封装,只能在TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求,LwIP 中可以接收多个客户端的连接,因此参数backlog 指定了请求队列的大小.
/** @ingroup socket */
#define listen(s,backlog) lwip_listen(s,backlog)
int
lwip_listen(int s, int backlog);
3.5 accept()
accept()函数与netconn_accept()函数作用一样,用于TCP 服务器中,等待着远端主机的连接请求,并且建立一个新的TCP 连接,在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态。accept()函数的调用会阻塞应用线程直至与远程主机建立TCP 连接。参数addr 是一个返回结果参数,它的值由accept()函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。同时函数返回一个int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1.
/** @ingroup socket */
#define accept(s,addr,addrlen) lwip_accept(s,addr,addrlen)
int
lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);
3.6 read()、recv()、recvfrom()
read()与recv()函数的核心是调用recvfrom()函数,而recvfrom()函数是基于netconn_recv()函数来实现的,recv()与read()函数用于从Socket 中接收数据,它们可以是TCP 协议和UDP 协议
/** @ingroup socket */
#define read(s,mem,len) lwip_read(s,mem,len)
ssize_t
lwip_read(int s, void *mem, size_t len)
{
return lwip_recvfrom(s, mem, len, 0, NULL, NULL);
}
/** @ingroup socket */
#define recv(s,mem,len,flags) lwip_recv(s,mem,len,flags)
ssize_t
lwip_recv(int s, void *mem, size_t len, int flags)
{
return lwip_recvfrom(s, mem, len, flags, NULL, NULL);
}
ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
men 参数记录了接收数据的缓存起始地址,
len 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。
recv()函数包含一个flags 参数,我们暂时可以直接忽略它,设置为0 即可。注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃.
3.7 sendto()
函数主要是用于UDP 协议传输数据中,它向另一端的UDP 主机发送一个UDP 报文,本质上是对netconn_send()函数的封装,参数data 指定了要发送数据的起始地址,而size 则指定数据的长度,参数flag 指定了发送时候的一些处理,比如外带数据等,此时我们不需要理会它,一般设置为0 即可,参数to 是一个指向sockaddr 结构体的指针,在这里需要我们自己提供远端主机的IP 地址与端口号,并且用tolen 参数指定这些信息的长度
/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen) lwip_sendto(s,dataptr,size,flags,to,tolen)
ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
3.8 send()
send()函数可以用于UDP 协议和TCP 连接发送数据。在调用send()函数之前,必须使用connect()函数将远端主机的IP 地址、端口号与Socket 连接结构进行绑定。对于UDP 协议,send()函数将调用lwip_sendto()函数发送数据,而对于TCP 协议,将调用netconn_write_partly()函数发送数据。相对于sendto()函数,参数基本是没啥区别的,但无需我们设置远端主机的信息,更加方便操作,因此这个函数在实际中使用也是很多的
/** @ingroup socket */
#define send(s,dataptr,size,flags) lwip_send(s,dataptr,size,flags)
ssize_t
lwip_send(int s, const void *data, size_t size, int flags);
3.9 write()
这个函数一般用于处于稳定的TCP 连接中传输数据,当然也能用于UDP 协议中,它也是基于lwip_send 上实现的,但是无需我们设置flag 参数
/** @ingroup socket */
#define write(s,dataptr,len) lwip_write(s,dataptr,len)
ssize_t
lwip_write(int s, const void *data, size_t size)
{
return lwip_send(s, data, size, 0);
}
3.10 close()
close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对netconn_delete()函数的封装(真正处理的函数是netconn_prepare_delete()),如果连接是TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是UDP 协议,将直接释放UDP 控制块的内容
/** @ingroup socket */
#define close(s) lwip_close(s)
int
lwip_close(int s);
3.11 ioctl()、ioctlsocket()
两个函数,其实是一样的,本质是宏定义,都是调用lwip_ioctl()函数,它用于获取与设置套接字相关的操作参数.
s:一个标识套接口的描述字。
cmd:对套接口s的操作命令。
argp:指向cmd命令所带参数的指针
参数cmd 指明对套接字的操作命令,在LwIP中只支持FIONREAD 与FIONBIO 命令:
- FIONREAD 命令确定套接字s 自动读入的数据量,这些数据已经被接收,但应用线程并未读取的,所以可以使用这个函数来获取这些数据的长度,在这个命令状态下,argp 参数指向一个无符号长整型,用于保存函数的返回值(即未读数据的长度)。如果套接字是SOCK_STREAM类型,则FIONREAD 命令会返回recv()函数中所接收的所有数据量,这通常与在套接字接收缓存队列中排队的数据总量相同;而如果套接字是SOCK_DGRAM类型的,则FIONREAD 命令将返回在套接字接收缓存队列中排队的第一个数据包大小。
- FIONBIO 命令用于允许或禁止套接字的非阻塞模式。在这个命令下,argp 参数指向一个无符号长整型,如果该值为0 则表示禁止非阻塞模式,而如果该值非0 则表示允许非阻塞模式则。当创建一个套接字的时候,它就处于阻塞模式,也就是
说非阻塞模式被禁止,这种情况下所有的发送、接收函数都会是阻塞的,直至发送、接收成功才得以继续运行;而如果是非阻塞模式下,所有的发送、接收函数都是不阻塞的,如果发送不出去或者接收不到数据,将直接返回错误代码给用户,这就需要用户对这些“意外”情况进行处理,保证代码的健壮性,这与BSD Socket 是一致的。
/** @ingroup socket */
#define ioctlsocket(s,cmd,argp) lwip_ioctl(s,cmd,argp)
/** @ingroup socket */
#define ioctl(s,cmd,argp) lwip_ioctl(s,cmd,argp)
int
lwip_ioctl(int s, long cmd, void *argp);
3.12 setsockopt()
/** @ingroup socket */
#define setsockopt(s,level,optname,opval,optlen) lwip_setsockopt(s,level,optname,opval,optlen)
int
lwip_setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
这个函数是用于设置套接字的一些选项的,参数level 有多个常见的选项,如:
SOL_SOCKET:表示在Socket 层。
IPPROTO_TCP:表示在TCP 层。
IPPROTO_IP: 表示在IP 层。
参数optname 表示该层的具体选项名称,比如:
- 1. 对于SOL_SOCKET 选项,可以是SO_REUSEADDR(允许重用本地地址和端口)、SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、SO_RCVBUF(设置发送数据缓冲区大小)等等。
- 2. 对于IPPROTO_TCP 选项,可以是TCP_NODELAY(不使用Nagle 算法)、TCP_KEEPALIVE(设置TCP 保活时间)等等。
- 3. 对于IPPROTO_IP 选项,可以是IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等等。
3.13 getsockopt()
这个函数与setsockopt()函数的选项参数及名称都是差不多的,只不过是作用是获得这些选项信息在这里就不过多讲解
4. 实验例程

4.1 TCP Server

#define PORT 5001
#define RECV_DATA (1024)
static void
tcpecho_thread(void *arg)
{
int sock = -1,connected;
char *recv_data;
struct sockaddr_in server_addr,client_addr;
socklen_t sin_size;
int recv_data_len;
recv_data = (char *)pvPortMalloc(RECV_DATA);
if (recv_data == NULL)
{
printf("No memory\n");
goto __exit;
}
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
printf("Socket error\n");
goto __exit;
}
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1)
{
printf("Unable to bind\n");
goto __exit;
}
if (listen(sock, 5) == -1)
{
printf("Listen error\n");
goto __exit;
}
while(1)
{
sin_size = sizeof(struct sockaddr_in);
connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);
printf("new client connected from (%s, %d)\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
{
int flag = 1;
setsockopt(connected,
IPPROTO_TCP, /* set option at TCP level */
TCP_NODELAY, /* name of option */
(void *) &flag, /* the cast is historical cruft */
sizeof(int)); /* length of option value */
}
while(1)
{
recv_data_len = recv(connected, recv_data, RECV_DATA, 0);
if (recv_data_len <= 0)
break;
printf("recv %d len data\n",recv_data_len);
write(connected,recv_data,recv_data_len);
}
if (connected >= 0)
closesocket(connected);
connected = -1;
}
__exit:
if (sock >= 0) closesocket(sock);
if (recv_data) free(recv_data);
}
4.2 TCP Client

#define PORT 5001
#define IP_ADDR "192.168.0.100"
static void client(void *thread_param)
{
int sock = -1;
struct sockaddr_in client_addr;
uint8_t send_buf[]= "This is a TCP Client test...\n";
while(1)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
printf("Socket error\n");
vTaskDelay(10);
continue;
}
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(PORT);
client_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));
if (connect(sock,
(struct sockaddr *)&client_addr,
sizeof(struct sockaddr)) == -1)
{
printf("Connect failed!\n");
closesocket(sock);
vTaskDelay(10);
continue;
}
printf("Connect to iperf server successful!\n");
while (1)
{
if(write(sock,send_buf,sizeof(send_buf)) < 0)
break;
vTaskDelay(1000);
}
closesocket(sock);
}
}
注意: PC与单板连接的时候,网络调试助手,必须保证防火墙是允许通信的,否则可能被拦截而造成失败。

本文详细介绍了lwIP中的Socket接口编程,包括Socket概述、LwIP中的Socket以及Socket API,如socket(), bind(), connect(), listen(), accept()等,并提供了实验例程,帮助开发者理解如何在LwIP中进行TCP Server和TCP Client的编程。"
103002342,8072409,卷积神经网络详解与应用,"['深度学习', '神经网络', '卷积层', '激活函数', '图像识别']
7826





