4.使用lwip的socket通信
4.1 socket概念
Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用 Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。本章讲解的是 LwIP 中的 Socket 编程接口,因为 LwIP 作者为了能让更多开发者直接上手 LwIP 的编程,专门设计了 LwIP 的第三种编程接口——Socket API,它兼容 BSDSocket。
Socket 虽然是能在多平台移植,但是 LwIP 中的 Socket 并不完善,因为 LwIP 设计之初就是为了在嵌入式平台中使用,它只实现了完整 Socket 的部分功能,不过,在嵌入式平台中,这些功能早已足够。
在 Socket 中,它使用一个套接字来记录网络的一个连接,套接字是一个整数,就像我们操作文件一样,利用一个文件描述符,可以对它打开、读、写、关闭等操作,类似的,在网络中,我们也可以对 Socket 套接字进行这样子的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。
4.2 socket API介绍
4.2.1 socket() API介绍
这个函数的功能是向内核申请一个套接字,lwip中的socket最终实现是lwip_socket,但是为了兼容BSD socket,所以重新define了一下,我们来看下
/** @ingroup socket */
#define socket(domain,type,protocol) lwip_socket(domain,type,protocol)
参数:
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。
4.2.2 bind() API介绍
用于服务器端绑定套接字与网卡信息,lwip中的bind最终实现是lwip_bind,但是为了兼容BSD bind,所以重新define了一下,我们来看下
/** @ingroup socket */
#define bind(s,name,namelen) lwip_bind(s,name,namelen)
参数:
s :是表示要绑定的 Socket 套接字,注意了,这个套机字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。
name: 是一个指向 sockaddr 结构体的指针,其中包含了网卡的 IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了 sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用 bind() 的时候,只需要直接填写相关字段即可
namelen: 指定了 name 结构体的长度。
下面我们来说下name的struct,如下:
struct sockaddr {
u8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
为了便于理解,lwip重新定义了一下这个结构体
/* members are in network byte order */
struct sockaddr_in {
u8_t sin_len;
sa_family_t sin_family; /* 协议簇 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN];
};
4.2.3 connect() API介绍
它用于客户端中,将 Socket 与远端 IP 地址、端口号进行绑定,在 TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个 TCP 连接请求),并最终建立新的 TCP 连接,而对于 UDP协议来说,调用这个函数只是在 UDP 控制块中记录远端 IP 地址与端口号,而不发送任何数据,参数信息与 bind() 函数是一样的
/** @ingroup socket */
#define connect(s,name,namelen) lwip_connect(s,name,namelen)
4.2.4 listen() API介绍
只能在 TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求, LwIP 中可以接收多个客户端的连接,因此参数 backlog 指定了请求队列的大小
/** @ingroup socket */
#define listen(s,backlog) lwip_listen(s,backlog)
4.2.5 accept() API介绍
用于 TCP 服务器中,等待着远端主机的连接请求,并且建立一个新的 TCP 连接,在调用这个函数之前需要通过调用 listen() 函数让服务器进入监听状态。accept() 函数的调用会阻塞应用线程直至与远程主机建立 TCP 连接。参数 addr 是一个返回结果参数,它的值由 accept() 函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。同时函数返回一个 int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1
/** @ingroup socket */
#define accept(s,addr,addrlen) lwip_accept(s,addr,addrlen)
4.2.6 read()、recv()、recvfrom() API介绍
read() 与 recv() 函数的核心是调用 recvfrom() 函数, recv() 与 read() 函数用于从 Socket 中接收数据,它们可以是 TCP 协议和 UDP 协议!
/** @ingroup socket */
#define recv(s,mem,len,flags) lwip_recv(s,mem,len,flags)
/** @ingroup socket */
#define recvmsg(s,message,flags) lwip_recvmsg(s,message,flags)
/** @ingroup socket */
#define recvfrom(s,mem,len,flags,from,fromlen) lwip_recvfrom(s,mem,len,flags,from,fromlen)
men 参数记录了接收数据的缓存起始地址,len 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为 0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。recv() 函数包含一个 flags 参数,我们暂时可以直接忽略它,设置为 0 即可。注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃。
4.2.7 sendto() API介绍
这个函数主要是用于 UDP 协议传输数据中,它向另一端的 UDP 主机发送一个 UDP 报文,参数 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)
4.2.8 sendto() API介绍
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)
4.2.9 write() API介绍
这个函数一般用于处于稳定的 TCP 连接中传输数据,当然也能用于 UDP 协议中,它也是基于lwip_send 上实现的,但是无需我们设置 flag 参数
/** @ingroup socket */
#define write(s,dataptr,len) lwip_write(s,dataptr,len)
4.2.10 close() API介绍
close() 函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对 netconn_delete() 函数的封装(真正处理的函数是 netconn_prepare_delete()),如果连接是 TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是 UDP 协议,将直接释放 UDP 控制块的内容!
/** @ingroup socket */
#define close(s) lwip_close(s)
|