文章目录
一、BSD Socket简介
BSD Socket最初是由加州伯克利大学为Unix系统开发出来的,因此也被称为伯克利套接字(Internet Berkeley Sockets),它是一种采用C语言进程间通信库的应用程序接口(API),经常用在计算机网络间的通信,大多数其他的编程语言也都使用类似的接口。
BSD Socket作为一种API,允许不同主机或者同一个计算机上的不同进程之间的通信,它支持多种I/O设备和驱动,但是具体的实现是依赖操作系统的。这种接口对于TCP/IP是必不可少的,所以是互联网的基础技术之一,所有现代的操作系统都实现了BSD Socket API,因为它已经是连接互联网的标准接口了,也是当前主机网络程序设计领域的事实标准。
为了能更大程度上方便开发者将其他平台上的网络应用程序移植到LwIP上,也为了能让更多开发者快速上手LwIP,LwIP内核作者为LwIP设计了第三种应用程序编程接口,即Socket API,兼容于BSD Socket,但是受嵌入式处理器资源和性能的限制,部分Socket接口并未在LwIP中完全实现,如果想了解完整的BSD Socket可以参考Berkeley套接字和BSD套接字API两篇文章。同时,Socket API是基于Sequential API来实现的,所以应用程序的执行效率较基于后者实现的程序效率更低,抽象程度越高,执行效率的损失越大,读者需要平衡取舍执行效率与通用可移植性。
BSD中用一个套接字记录网络中的一个连接,套接字就像一个普通的文件,应用程序可以像操作普通文件那样操作它,例如打开、关闭、读写等。文件描述符是一个整数,所以套接字描述符也是一个整数,通过它可以索引到内核中描述连接的具体结构。
二、Socket API实现
2.1 Socket结构描述
在LwIP抽象出来的Socket API中,内核为用户提供了最多NUM_SOCKETS个可使用的socket描述符,并定义了结构体lwip_socket(对netconn结构的封装和增强)来描述一个具体连接。内核定义了数组sockets,通过一个Socket描述符就可以索引得到相应的连接结构lwip_socket,从而实现对连接的操作。连接结构lwip_socket的数据结构实现如下:
// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c
#define NUM_SOCKETS MEMP_NUM_NETCONN
/** Contains all internal pointers and states used for a socket */
struct lwip_sock {
/** sockets currently are built on netconns, each socket has one netconn */
struct netconn *conn;
/** data that was left from the previous read */
void *lastdata;
/** offset in the data that was left from the previous read */
u16_t lastoffset;
/** 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;
/** last error that occurred on this socket */
int err;
/** counter of how many threads are waiting for this socket using select */
int select_waiting;
};
/** The global array of available sockets */
static struct lwip_sock sockets[NUM_SOCKETS];
lwip_socket结构是对连接结构netconn的再次封装,在内核内部,对lwip_socket的操作最终都会映射到对netconn结构的操作上。
描述socket的结构体lwip_sock相对比较简单,它基于内核netconn来实现所有逻辑,conn指向了与socket对应的netconn结构;另外在socket数据接收时,其实它是利用netconn相关的接收函数获得一个pbuf(对于TCP)或者一个netbuf(对于UDP)数据,而这二者封装的数据可能大于socket用户指定的数据接收长度,因此在这种情况下,这两个数据包需要暂时保存在socket中,以待用户下一次读取,这里lastdata就用于指向未被用户完全读取的数据包,而lastoffset则指向了未读取的数据在数据包中的偏移。
lwip_sock最后的五个字段是为select机制实现时使用,select函数可通过事件机制监听一个或多个套接字状态的变化,常用于并发服务器编程,后面再专门介绍其实现过程。
2.2 Socket API接口函数
- socket
函数原型:int socket(int domain, int type, int protocol)
该函数功能是向内核申请一个套接字,本质上,socket是对Sequential API函数netconn_new的封装,三个参数分别如下:
参数名称 | 参数取值 |
---|---|
domain 为创建的套接字指定使用的协议簇 |
AF_INET 表示IPv4网络协议 AF_INET6 表示IPv6 AF_UNIX 表示本地套接字(使用一个文件) |
type 指定协议簇中的具体服务类型 |
SOCK_STREAM (可靠数据流交付服务,比如TCP) SOCK_DGRAM (无连接数据报交付服务,比如UDP) SOCK_RAW (原始套接字,比如RAW) |
protocol 指定实际使用的具体协议 |
常见的有IPPROTO_TCP、IPPROTO_UDP等 若设置为"0",表示根据前两个参数使用缺省协议 |
该函数的返回值为一个有效的socket描述符,内核使用这个函数就可以索引到描述连接的具体结构lwip_socket。当申请失败时,该函数返回-1。该函数的实现代码如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
#define socket(a,b,c) lwip_socket(a,b,c)
// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c
int lwip_socket(int domain, int type, int protocol)
{
struct netconn *conn;
int i;
/* create a netconn */
switch (type) {
case SOCK_RAW:
conn = netconn_new_with_proto_and_callback(NETCONN_RAW, (u8_t)protocol, event_callback);
break;
case SOCK_DGRAM:
conn = netconn_new_with_callback( (protocol == IPPROTO_UDPLITE) ?
NETCONN_UDPLITE : NETCONN_UDP, event_callback);
break;
case SOCK_STREAM:
conn = netconn_new_with_callback(NETCONN_TCP, event_callback);
if (conn != NULL) {
/* Prevent automatic window updates, we do this on our own! */
netconn_set_noautorecved(conn, 1);
}
break;
default:
set_errno(EINVAL);
return -1;
}
if (!conn) {
set_errno(ENOBUFS);
return -1;
}
i = alloc_socket(conn, 0);
if (i == -1) {
netconn_delete(conn);
set_errno(ENFILE);
return -1;
}
conn->socket = i;
set_errno(0);
return i;
}
static int alloc_socket(struct netconn *newconn, int accepted)
{
int i;
SYS_ARCH_DECL_PROTECT(lev);
/* allocate a new socket identifier */
for (i = 0; i < NUM_SOCKETS; ++i) {
/* Protect socket array */
SYS_ARCH_PROTECT(lev);
if (!sockets[i].conn) {
sockets[i].conn = newconn;
/* The socket is not yet known to anyone, so no need to protect
after having marked it as used. */
SYS_ARCH_UNPROTECT(lev);
sockets[i].lastdata = NULL;
sockets[i].lastoffset = 0;
sockets[i].rcvevent = 0;
/* TCP sendbuf is empty, but the socket is not yet writable until connected
* (unless it has been created by accept()). */
sockets[i].sendevent = (newconn->type == NETCONN_TCP ? (accepted != 0) : 1);
sockets[i].errevent = 0;
sockets[i].err = 0;
sockets[i].select_waiting = 0;
return i;
}
SYS_ARCH_UNPROTECT(lev);
}
return -1;
}
- bind
函数原型:int bind(int s, const struct sockaddr *name, socklen_t namelen)
该函数功能是将一个套接字与本地地址信息进行绑定,本质上,bind是对Sequential API函数netconn_bind的封装。作为服务器程序,通常需要调用该函数将套接字绑定到本地的知名端口号上,这样才能响应客户端的连接请求。参数s记录了将要进行绑定的套接字对象;参数name指向一个sockaddr结构体,包含了本地IP地址和端口号等信息;参数namelen指出了结构体的长度。结构体sockaddr的定义如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
/* members are in network byte order */
struct sockaddr {
u8_t sa_len;
u8_t sa_family;
char sa_data[14];
};
struct sockaddr_in {
u8_t sin_len;
u8_t sin_family;
u16_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
// rt-thread\components\net\lwip-1.4.1\src\include\ipv4\lwip\inet.h
/** For compatibility with BSD code */
struct in_addr {
u32_t s_addr;
};
结构体sockaddr中的sa_family指向该套接字所使用的协议簇,sa_data指向了bind需要的一些本地地址信息,前2个字节用于记录端口号port,接下来4个字节用于记录IP地址,剩余的8个字节用于传递其他信息,这里暂未用到。由于sa_data以连续空间的方式存在,如果用户要填写其中的IP字段和端口port字段,会显得比较麻烦,因此socket中定义了另一种sockaddr_in结构,它与sockaddr结构对等,只是从中抽出IP地址和端口号port,方便于用于的编程操作。
该函数绑定成功时返回0,绑定失败时返回-1。该函数的实现代码如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
#define bind(a,b,c) lwip_bind(a,b,c)
// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c
int lwip_bind(int s, const struct sockaddr *name, socklen_t namelen)
{
struct lwip_sock *sock;
ip_addr_t local_addr;
u16_t local_port;
err_t err;
const struct sockaddr_in *name_in;
sock = get_socket(s);
if (!sock) {
return -1;
}
/* check size, familiy and alignment of 'name' */
name_in = (const struct sockaddr_in *)(void*)name;
inet_addr_to_ipaddr(&local_addr, &name_in->sin_addr);
local_port = name_in->sin_port;
err = netconn_bind(sock->conn, &local_addr, ntohs(local_port));
if (err != ERR_OK) {
sock_set_errno(sock, err_to_errno(err));
return -1;
}
sock_set_errno(sock, 0);
return 0;
}
- connect
函数原型:int connect(int s, const struct sockaddr *name, socklen_t namelen)
该函数功能与bind函数相对应:将套接字与目的地址信息进行绑定。该函数本质上是对Sequential API函数netconn_connect的封装,作为客户端程序,通常需要使用该函数来绑定服务器的地址信息。对于TCP连接,调用这个函数会导致客户端与服务器之间发生连接握手过程,并最终建立一条稳定的连接;对于UDP连接,该函数调用不会有任何数据包被发送,只是在连接结构中记录下服务器的地址信息。当调用成功时,函数返回0;否则返回-1。
- getsockname / getpeername
函数原型:int getsockname (int s, struct sockaddr *name, socklen_t *namelen);
int getpeername (int s, struct sockaddr *name, socklen_t *namelen)
该函数功能是获取该套接字的本地地址信息 / 远端地址信息。该函数本质上是对Sequential API函数netconn_addr / netconn_peer的封装,参数name保存了从套接字获取的地址信息,参数namelen保存了从套接字获取的name结构体长度信息。当调用成功时,函数返回0;否则返回-1。
- listen
函数原型:int listen(int s, int backlog)
该函数只能在TCP服务器程序中使用,作用是将一个套接字置为侦听状态,以等待客户端的连接请求。该函数本质上是对Sequential API函数netconn_listen的封装,内核同时接收到多个连接请求时,需要对这些请求进行排队处理,参数backlog指明了该套接字上连接请求队列的最大长度。当调用成功时,函数返回0;否则返回-1。
- accept
函数原型:int accept(int s, struct sockaddr *addr, socklen_t *addrlen)
该函数也只能在TCP服务器程序中使用,作用是从套接字的连接请求队列中获取一个新建立的连接,如果请求队列为空,该函数会阻塞,直至新连接到来。该函数本质上是对Sequential API函数netconn_accept的封装,当接收到新连接后,连接另一端(客户端)的地址信息会被填入到地址结构addr中,而对应地址信息的长度被记录到addrlen中。函数返回新连接的套接字描述符,若调用失败,函数返回-1。
- send / sendto
函数原型:int send(int s, const void *dataptr, size_t size, int flags);
int sendto(int s, const void *dataptr, size_t size, int flags, const struct sockaddr *to, socklen_t tolen)
函数sendto主要在UDP连接中使用,作用是向另一端发送UDP报文。该函数本质上是对Sequential API函数netconn_send的封装,参数data和size分别指出了待发送数据的起始地址和长度;flags指明数据发送时的特殊处理,例如带外数据、紧急数据等,通常设置为0;参数to和tolen分别指明了目的地址信息及信息的长度,地址信息包含了目的IP地址和目的端口号。调用成功后,函数返回成功发送的字节数,出错则返回-1。
另一个函数send主要用于在一条已建立的连接上发送数据,因此不需要在参数中包含目的地址信息。该函数即可用于TCP程序,也可用于UDP程序,其本质是对Sequential API函数netconn_write和netconn_send的封装。调用成功后,函数返回成功发送的字节数,出错则返回-1。该函数的实现代码如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\sockets.h
#define send(a,b,c,d) lwip_send(a,b,c,d)
#define sendto(a,b,c,d,e,f) lwip_sendto(a,b,c,d,e,f)
/* Flags we can use with send and recv. */
#define MSG_PEEK 0x01 /* Peeks at an incoming message */
#define MSG_WAITALL 0x02 /* Unimplemented: Requests that the function block until the full amount of data requested can be returned */
#define MSG_OOB 0x04 /* Unimplemented: Requests out-of-band data. The significance and semantics of out-of-band data are protocol-specific */
#define MSG_DONTWAIT 0x08 /* Nonblocking i/o for this operation only */
#define MSG_MORE 0x10 /* Sender will send more */
// rt-thread\components\net\lwip-1.4.1\src\api\sockets.c
int lwip_send(int s, const void *data, size_t size, int flags)
{
struct lwip_sock *sock;
err_t err;
u8_t write_flags;
size_t written;
sock = get_socket(s);
if (!sock) {
return -1;
}
if (sock->conn->type != NETCONN_TCP) {
return lwip_sendto(s, data, size, flags, NULL, 0);
}
write_flags = NETCONN_COPY |
((flags & MSG_MORE) ? NETCONN_MORE : 0) |
((flags & MSG_DONTWAIT) ? NETCONN_DONTBLOCK : 0);
written = 0;
err = netconn_write_partly(sock->conn, data, size, write_flags, &written);
sock_set_errno(sock, err_to_errno(err));
return (err == ERR_OK ? (int)written : -1);
}
int lwip_sendto(int s, const void *data, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen)
{
struct lwip_sock *sock;
err_t err;
u16_t short_size;
const struct sockaddr_in *to_in;
u16_t remote_port;
struct netbuf buf;
sock = get_socket(s);
if (!sock) {
return -1;
}
if (sock->conn->type == NETCONN_TCP) {
return lwip_send(s, data, size, flags);
}
/* @todo: split into multiple sendto's? */
short_size = (u16_t)size;
to_in = (const struct sockaddr_in *)(void*)to;
/* initialize a buffer */
buf.p = buf.ptr = NULL;
buf.flags = 0;
if (to) {
inet_addr_to_ipaddr(&buf.addr, &to_in->sin_addr);
remote_port = ntohs(to_in->sin_port);
netbuf_fromport(&buf) = remote_port;
} else {
remote_port = 0;
ip_addr_set_any(&buf.addr);
netbuf_fromport(&buf) = 0;
}
/* Allocate a new netbuf and copy the data into it. */
if (netbuf_alloc(&buf, short_size) == NULL) {
err = ERR_MEM;
} else {
if (sock->conn->type != NETCONN_RAW) {
u16_t chksum = LWIP_CHKSUM_COPY(buf.p->payload, data, short_size);
netbuf_set_chksum(&buf, chksum);
err = ERR_OK;
} else
err = netbuf_take(&buf, data, short_size);
}
if (err == ERR_OK) {
/* send the data */
err = netconn_send(sock->conn, &buf);
}
/* deallocated the buffer */
netbuf_free(&buf);
sock_set_errno(sock, err_to_errno(err)