写在最前面
- 在基于lwip socket api完成tcp服务器端代码过程中,因为个人对sokcet部分原理和函数了解不清楚,出现了很多问题,故本文想通过对socket涉及结构体和函数的仔细解析将soket通信了解清楚。
- 在lwip中查看lwip-2.1.1/src/api/sockets.c 以及对应的sockets.h源码文件【不止这些,越看越多o(╥﹏╥)o】
- 写的应该有不准确的地方,还只是个人粗浅(浅薄)的理解
- 参考链接顶头上:https://blog.csdn.net/wuhenyouyuyouyu/article/details/89187386
一、函数与涉及数据结构说明
在sockets.h中通过重定义,将lwip_socket,lwip_recv,lwip_send等函数定义为socket, recv, send 等,因而在使用时采用socket(),recv(),send()等函数,实际实现查看lwip_socket(),lwip_recv()等。
1. socket 创建函数: socket(domain,type,protocol) <---->int lwip_socket(int domain, int type, int protocol)
-
调用过程如下图,整个过程围绕着struct netconn *conn 结构体指针根据函数参数type的不同,完成结构体的字段的初始化
-
数据结构查看:
-
netconn结构体内容如下,其包含了连接类型,连接状态,协议对应的PCB,以及接收数据存储邮箱,TCP建立连接对应的邮箱
struct netconn { /** type of the netconn (TCP, UDP or RAW) */ enum netconn_type type; /** current state of the netconn */ enum netconn_state state; /** the lwIP internal protocol control block */ union { struct ip_pcb *ip; struct tcp_pcb *tcp; struct udp_pcb *udp; struct raw_pcb *raw; } pcb; /** the last asynchronous unreported error this netconn had */ err_t pending_err; #if !LWIP_NETCONN_SEM_PER_THREAD /** sem that is used to synchronously execute functions in the core context */ sys_sem_t op_completed; #endif /** mbox where received packets are stored until they are fetched by the netconn application thread (can grow quite big) */ sys_mbox_t recvmbox;//接收数据的数据包存储的邮箱 #if LWIP_TCP /** mbox where new connections are stored until processed by the application thread */ sys_mbox_t acceptmbox; #endif /* LWIP_TCP */ #if LWIP_SOCKET int socket; #endif /* LWIP_SOCKET */ u8_t flags; #if LWIP_TCP struct api_msg *current_msg; #endif /* LWIP_TCP */ netconn_callback callback; };
-
lwip_sock 结构体:因整个socket的实现是基于netconn的,socket相当于在netconn的基础上包了一层,故而,其结构体实际内涵为netonn结构体指针,另外包含socket的poll和select模式下的一些回调函数相关字段
/** 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 */ 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; /** counter of how many threads are waiting for this socket using select */ SELWAIT_T select_waiting; #endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */ };
-
-
函数返回值: 类型为int,实际含义为socket标记符,在lwip配置文件中,可配置允许的最大套接字个数,对应字段为MEMP_NUM_NETCONN。在alloc_socket()函数中,会从0->NUM_SOCKETS逐次分配socket,若当前第i个socket已经初始化,则分配第i+1个socket。具体分配从sockets.c的全局变量/** The global array of available sockets */static struct lwip_sock sockets[NUM_SOCKETS]数组中进行分配。
-
参数:
- domain: ipv4(AF_INET)或ipv6(AF_INET6)
- type: 三种,分别为:SOCK_STREAM<–>tcp,SOCK_DGRAM<–>udp,SOCK_RAW<–>raw
2. socket 关闭函数:socket(domain,type,protocol) <----> int lwip_close(int s)
- 实际调用netconn_prepare_delete(sock->conn),该函数采用回调机制,调用lwip_netconn_do_delconn,根据udp和tcp断连的不同,udp直接采用udp_remove,而tcp需要四次挥手,调用lwip_netconn_do_close_internal(struct netconn *conn WRITE_DELAYED_PARAM)来实现tcp关闭连接过程
- 等netconn释放结束后,通过free_socket(struct lwip_sock *sock, int is_tcp)来释放lwip_socket结构体
3. socket绑定ip,端口函数:bind(s,name,namelen) <----> lwip_bind(int s, const struct sockaddr *name, socklen_t namelen)
- 实际调用:netconn_bind(sock->conn, &local_addr, local_port),最终是对socket->netconn>pcb的ip和port进行赋值操作
4. 监听函数 :listen(s,backlog) <----> int lwip_listen(int s, int backlog)
- 实际调用:netconn_listen_with_backlog(sock->conn, (u8_t)backlog),再次调用回调函数void lwip_netconn_do_listen(void *m),最终调用tcp_listen_with_backlog_and_err(msg->conn->pcb.tcp, backlog, &err)函数创建lpcb,将lpcb赋值给conn中的tcp的pcb中的callback_arg,随后将状态改为listen状态
5. socket数据接收函数:recv(s,mem,len,flags) <----> lwip_recv(s,mem,len,flags)
-
函数间调用关系如下图,蓝色虚线框内为netconn相关内容,从调用关系可以看出,整个接收函数从socket层调用到了netconn层,最终从conn的接收邮箱中将数据读出至pbuf中,将pbuf赋值给socket->lastdata.pbuf,用了socket通信后不好的地方在于返回值只有0, -1 以及接收的数据长度,不能很好的体现出接收数据的状态,还需要再好好看看有没有其他方式可以获取到当前连接的状态
-
那么数据又是怎么放入邮箱的呢?
- 全局搜索conn->recvmbox 可以追溯到了listen阶段:在lwip_netconn_do_listen()函数中,创建lpcb成功后,会删除掉旧的接收邮箱,创建接受连接的邮箱,同时注册accept的回调函数tcp_accept(msg->conn->pcb.tcp, accept_function),在accept_function中会设置TCP setup_tcp(newconn),在该函数中,注册了tcp的接收,发送,以及错误回调函数
setup_tcp(struct netconn *conn) { struct tcp_pcb *pcb; pcb = conn->pcb.tcp; tcp_arg(pcb, conn); tcp_recv(pcb, recv_tcp); tcp_sent(pcb, sent_tcp); tcp_poll(pcb, poll_tcp, NETCONN_TCP_POLL_INTERVAL); tcp_err(pcb, err_tcp); }
-
查看tcp_recv(pcb, recv_tcp),可以看到就是赋值的过程,将recv_tcp()函数注册为pcb的接收回调函数
tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv) { LWIP_ASSERT_CORE_LOCKED(); if (pcb != NULL) { LWIP_ASSERT("invalid socket state for recv callback", pcb->state != LISTEN); pcb->recv = recv; } }
-
进一步查看静态函数 static err_t recv_tcp,可以看到将pbuf放入接收邮箱(recvmbox)的过程
sys_mbox_trypost(&conn->recvmbox, msg)
-
至此,我们还是没有解决,放入到邮箱的数据是怎么来的?
查看野火写的《LWIP应用开发实战指南》可以看到下图,在书中为图9-1,描述了一个数据从网卡到内核的过程
与图中稍有不同的是,在tcpip_input函数的实现中,调用了tcpip_inpkt()函数,该函数通过查看LWIP_TCPIP_CORE_LOCKING_INPUT是否为1决定了是否向tcpip_mbox投递消息。源代码如下:
err_t tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn) { #if LWIP_TCPIP_CORE_LOCKING_INPUT err_t ret; LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_inpkt: PACKET %p/%p\n", (void *)p, (void *)inp)); LOCK_TCPIP_CORE(); ret = input_fn(p, inp); UNLOCK_TCPIP_CORE(); return ret; #else /* LWIP_TCPIP_CORE_LOCKING_INPUT */ struct tcpip_msg *msg; LWIP_ASSERT("Invalid mbox", sys_mbox_valid_val(tcpip_mbox)); msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT); if (msg == NULL) { return ERR_MEM; } msg->type = TCPIP_MSG_INPKT; msg->msg.inp.p = p; msg->msg.inp.netif = inp; msg->msg.inp.input_fn = input_fn; if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) { memp_free(MEMP_TCPIP_MSG_INPKT, msg); return ERR_MEM; } return ERR_OK; #endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */ }
-
不管是否向tcpip_mbox的邮箱投递消息,殊途同归的是,最终都会调用ip4_input()->tcp_input()->tcp_process()->tcp_receive(),这部分的代码因为tcp接收的复杂性而比较长,看了又看,看会到tcp_input()函数,可以注意到在tcp_process()后,有一句
/* Notify application that data has been received. */ TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
其具体实现为,调用pcb的接收回调函数实现数据接收,这里便和socket(应用层)的setup_tcp()函数对应上了!!,也就是说这里会调用recv_tcp()函数,将pbuf放入到接收邮箱中!
此处,必须放上参考链接:https://blog.csdn.net/wuhenyouyuyouyu/article/details/89187386#define TCP_EVENT_RECV(pcb,p,err,ret) \ do { \ if((pcb)->recv != NULL) { \ (ret) = (pcb)->recv((pcb)->callback_arg,(pcb),(p),(err));\ } else { \ (ret) = tcp_recv_null(NULL, (pcb), (p), (err)); \ } \ } while (0)
-
未完待续。。。