TCP/IP应用层协议实现 - 数据收发send/recv(lwip)
Posted arm7star
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP应用层协议实现 - 数据收发send/recv(lwip)相关的知识,希望对你有一定的参考价值。
1、接收数据(recv)
lwip的recv最终调用lwip_recvfrom接收数据。https://blog.csdn.net/arm7star/article/details/117187409 《TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)》有介绍,传输层将数据发送到应用层的邮箱里面,lwip_recvfrom主要负责从邮箱读取数据。
检查上次接收的邮箱是否有数据待接收。(应用层接收数据时会指定长度,一次从邮箱接收到的数据可能大于应用层指定接收的数据,因此sock->lastdata记录上一次尚未接收完的数据)
do {
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom: top while sock->lastdata=%p\\n", (void*)sock->lastdata));
/* Check if there is data left from the last recv operation. */
if (sock->lastdata) {
buf = sock->lastdata; // 继续接收上一次从邮箱接收到的数据
} else {
如果lastdata为空,需要继续从邮箱读取数据。(接收数据,设置错误码用于判断recv返回原因)
} else {
/* If this is non-blocking call, then check first */
if (((flags & MSG_DONTWAIT) || (sock->flags & O_NONBLOCK)) && // 非阻塞模式
(sock->rcvevent <= 0)) { // rcvevent一定程度代表邮箱里面有多少邮件(多少数据块待接收,传输层每次接收到数据都将数据发送到邮箱里面,一个邮件就是一次接收到的数据)
if (off > 0) { // 如果有数据(应用层接收数据是循环读取邮箱,一次接收的数据比较小的时候,继续读取邮箱的下一个数据,off > 0表示前面的邮箱有读取到数据)
/* already received data, return that */
sock_set_errno(sock, 0); // 设置errno为0
return off; // 非阻塞模式返回当前已经读取到的数据
}
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom(%d): returning EWOULDBLOCK\\n", s));
sock_set_errno(sock, EWOULDBLOCK); // 非阻塞模式下,没有数据,设置errno为EWOULDBLOCK(应用层根据errno判断返回原因;代码这里有个问题,如果收到FIN之后,再次调用recv,那么这个错误码应该设置为连接关闭,否则非阻塞模式下,应用层无知道收到了FIN!!! 收到FIN之后,传输层不会再发送数据到应用层;这里应该根据阻塞模式收到NULL时的代码一样,设置errno;阻塞模式会调用netconn_recv,netconn_recv会设置错误返回NULL,因此阻塞模式不存在问题)
return -1; // 没有数据返回-1
}
/* No data was left from the previous operation, so we try to get
some from the network. */
sock->lastdata = buf = netconn_recv(sock->conn); // 阻塞模式,从邮箱读取/等待数据(rcvevent > 0时,邮箱有数据,读取数据,rcvevent <= 0时,邮箱没有数据,阻塞等待邮箱有数据)
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom: netconn_recv netbuf=%p\\n", (void*)buf));
if (!buf) { // buf为NULL(之前文章有介绍,传输层收到FIN报文时,会发送一个NULL的消息数据标记连接结束(对端调用了close),此次即为FIN的NULL数据;netconn_recv收到FIN的NULL消息后,netconn_recv会设置netconn状态为ERR_CLSD,再次调用netconn_recv也会返回NULL(接下来的off > 0的情况下,recv仍会接收到数据,errno为0,即使buf为NULL,应用层也判断不出连接已关闭,因此下次调用netconn_recv没有收到任何数据时,即可判断连接已关闭))
if (off > 0) { // 前面邮箱已经收到数据
/* already received data, return that */
sock_set_errno(sock, 0); // 设置errno为0
return off; // 返回收到的数据长度
}
/* We should really do some error checking here. */
LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_recvfrom(%d): buf == NULL!\\n", s));
sock_set_errno(sock, (((sock->conn->pcb.ip != NULL) && (sock->conn->err == ERR_OK))
? ETIMEDOUT : err_to_errno(sock->conn->err))); // 设置errno(连接已经关闭,没有收到数据,设置errno)
return 0; // 没有数据返回0
}
}
PUSH标志检查。(recv读取到PUSH标志后,即使没有收到指定长度的数据也会返回)
if (netconn_type(sock->conn) == NETCONN_TCP) {
LWIP_ASSERT("invalid copylen, len would underflow", len >= copylen);
len -= copylen;
if ( (len <= 0) ||
(buf->p->flags & PBUF_FLAG_PUSH) || // 收到的数据带有PUSH
(sock->rcvevent <= 0) ||
((flags & MSG_PEEK)!=0)) {
done = 1; // 设置done为1,recv返回
}
} else {
2、发送数据(send)
send比较简单。前面文章介绍过,发送的时候是将数据缓存到发送队列,发送队列及发送缓存有限,因此send存在阻塞。
发送的代码主要在tcp/ip线程执行,发送操作与tcp传输层的数据结构关系比较紧密,因此在一个线程里面串行处理,避免竞争。
tcp/ip线程调用do_write、do_writemore发送数据到发送队列。
tcp数据发送。
if ((msg->conn->pcb.tcp != NULL) && (msg->conn->type == NETCONN_TCP)) {
#if LWIP_TCP
msg->conn->state = NETCONN_WRITE; // 设置netconn的状态为写状态(传输层有数据被确认接收后,如果还有数据待写入发送队列,会根据这个状态来发送更多数据到发送队列里面)
/* set all the variables used by do_writemore */
LWIP_ASSERT("already writing", msg->conn->write_msg == NULL &&
msg->conn->write_offset == 0);
msg->conn->write_msg = msg;
msg->conn->write_offset = 0; // 写数据的偏移(第一次写入发送队列,从应用层数据的0偏移开始写)
#if LWIP_TCPIP_CORE_LOCKING
msg->conn->write_delayed = 0;
if (do_writemore(msg->conn) != ERR_OK) {
LWIP_ASSERT("state!", msg->conn->state == NETCONN_WRITE);
UNLOCK_TCPIP_CORE();
sys_arch_sem_wait(msg->conn->op_completed, 0);
LOCK_TCPIP_CORE();
LWIP_ASSERT("state!", msg->conn->state == NETCONN_NONE);
}
#else
do_writemore(msg->conn); // 调用do_writemore写发送队列(数据指针、偏移等都在msg->conn里面)
#endif
/* for both cases: if do_writemore was called, don't ACK the APIMSG! */
return; // 返回(这是在tcp/ip线程里面执行返回,不会导致应用层的send返回!!!)
#endif /* LWIP_TCP */
#if (LWIP_UDP || LWIP_RAW)
} else {
写发送队列。(do_writemore如果一次写不完,则写当前能写的数据,剩余的数据由传输层的sent事件触发调用do_writemore写发送队列)
static err_t
do_writemore(struct netconn *conn)
{
err_t err;
void *dataptr;
u16_t len, available;
u8_t write_finished = 0;
size_t diff;
LWIP_ASSERT("conn->state == NETCONN_WRITE", (conn->state == NETCONN_WRITE));
dataptr = (u8_t*)conn->write_msg->msg.w.dataptr + conn->write_offset; // 本次从dataptr开始写数据到发送队列
diff = conn->write_msg->msg.w.len - conn->write_offset; // 全部待写数据长度
if (diff > 0xffffUL) { /* max_u16_t */ // 待写数据太多
len = 0xffff; // 一次最多发送0xffff到发送队列
#if LWIP_TCPIP_CORE_LOCKING
conn->write_delayed = 1;
#endif
} else {
len = (u16_t)diff; // 本次可写发送队列数据的长度
}
available = tcp_sndbuf(conn->pcb.tcp); // 可用发送缓存大小
if (available < len) { // 可用发送缓存小于需要发送的数据
/* don't try to write more than sendbuf */
len = available; // 本次发送数据设置为可用发送缓存的大小
#if LWIP_TCPIP_CORE_LOCKING
conn->write_delayed = 1;
#endif
}
err = tcp_write(conn->pcb.tcp, dataptr, len, conn->write_msg->msg.w.apiflags);
LWIP_ASSERT("do_writemore: invalid length!", ((conn->write_offset + len) <= conn->write_msg->msg.w.len)); // 将数据添加到发送队列
if (err == ERR_OK) {
conn->write_offset += len; // 下次调用do_writemore时的数据偏移
if (conn->write_offset == conn->write_msg->msg.w.len) { // 下次发送数据的偏移等于数据的长度(所有数据已经发送到了发送队列里面)
/* everything was written */
write_finished = 1; // 写完成(send已经将所有数据发送到发送队列)
conn->write_msg = NULL; // 待写数据设置为空
conn->write_offset = 0;
/* API_EVENT might call tcp_tmr, so reset conn->state now */
conn->state = NETCONN_NONE; // netconn状态设置为NETCONN_NONE(传输层检测到NETCONN_NONE状态就不用判断应用层是否有更多数据需要写入发送队列,不用调用do_writemore)
}
err = tcp_output_nagle(conn->pcb.tcp); // 调用tcp_output_nagle尝试发送报文(如果没有数据在发送或者发送窗口没有满,则可能需要将本次发送队列的数据发送出去)
conn->err = err;
if ((err == ERR_OK) && (tcp_sndbuf(conn->pcb.tcp) <= TCP_SNDLOWAT)) {
API_EVENT(conn, NETCONN_EVT_SENDMINUS, len);
}
} else if (err == ERR_MEM) {
/* If ERR_MEM, we wait for sent_tcp or poll_tcp to be called
we do NOT return to the application thread, since ERR_MEM is
only a temporary error! */
/* tcp_enqueue returned ERR_MEM, try tcp_output anyway */
err = tcp_output(conn->pcb.tcp); // 没有更多内存,调用tcp_output发送报文(尽快让对端接收数据,尽快释放发送缓存)
#if LWIP_TCPIP_CORE_LOCKING
conn->write_delayed = 1;
#endif
} else {
/* On errors != ERR_MEM, we don't try writing any more but return
the error to the application thread. */
conn->err = err;
write_finished = 1;
}
if (write_finished) {
/* everything was written: set back connection state
and back to application task */
conn->state = NETCONN_NONE;
#if LWIP_TCPIP_CORE_LOCKING
if (conn->write_delayed != 0)
#endif
{
sys_sem_signal(conn->op_completed); // 所有数据发送到发送队列后,设置op_completed信号,应用层send等待op_completed,send调用返回
}
}
#if LWIP_TCPIP_CORE_LOCKING
else
return ERR_MEM;
#endif
return ERR_OK;
}
数据被接收,发送缓存被释放,发送更多数据到发送队列。
static err_t
sent_tcp(void *arg, struct tcp_pcb *pcb, u16_t len)
{
struct netconn *conn = arg;
LWIP_UNUSED_ARG(pcb);
LWIP_ASSERT("conn != NULL", (conn != NULL));
if (conn->state == NETCONN_WRITE) { // 有待写入发送队列的数据
LWIP_ASSERT("conn->pcb.tcp != NULL", conn->pcb.tcp != NULL);
do_writemore(conn); // 调用do_writemore将conn里面的数据写入发送队列(一次写不完,下次调用sent_tcp再次写发送队列),最后一次do_writemore写完所有数据到发送队列后,设置信号唤醒send线程
} else if (conn->state == NETCONN_CLOSE) {
do_close_internal(conn);
}
if (conn) {
if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT)) {
API_EVENT(conn, NETCONN_EVT_SENDPLUS, len);
}
}
return ERR_OK;
}
以上是关于TCP/IP应用层协议实现 - 数据收发send/recv(lwip)的主要内容,如果未能解决你的问题,请参考以下文章
Linux Kernel TCP/IP Stack — 协议栈发包处理流程
TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)