此篇博客用来记录 ESP32 TCP/UDP 相关应用层操作。首先列举出几个常用的 setsockopt option:
SO_REUSEADDR
启用地址复用,允许 socket 绑定处于 TIME-WAIT 状态的相同端口的 IP 地址SO_KEEPALIVE
启用 TCP 保活机制,用于服务器或客户端检测链接是否异常,默认值 IDLE:2 小时,INTVL:75 秒,CNT: 9 次SO_LINGER
设置函数close()
关闭 TCP 链接时的行为SO_BROADCAST
允许发送接收广播包,主要用于 IPv4 UDP 需要广播发送报文SO_SNDTIMEO
设置发送超时,适用于 send,write。如果超过时间,缓冲区还无法写入,会返回 -1 (错误为 ERR_WOULDBLOCK),如果写入一部分,返回写入的长度SO_RCVTIMEO
设置接收超时,适用于 read,recv,accept。如果超时时间内没收到数据,返回 -1,错误为 ERR_TIMEOUTSO_BINDTODEVICE
socket 接口绑定,用于多个接口的时候,让数据走指定的接口,类似于 bindIP_MULTICAST_TTL
设置 UDP 组播包时 IP 包头的 TTL,只针对 UDP 控制报文,默认 255TCP_NODELAY
设置是否禁止 Nagle(默认使能 Nagle 算法),使能 NODELAY 意味着禁用 Nagle 算法,允许发送小包,适用于敏感型,对延迟要求低的应用。禁用 NODELAY 意味这使能 Nagle 算法,数据在缓冲区累计到一定长度再发送,增大延迟,提高网络利用率,适用于吞吐要求高的应用。SO_ACCEPTCONN
查询 socket 是否处于 listen 状态SO_TYPE
查询 socket 类型是SOCK_RAW
,SOCK_STREAM
还是SOCK_DGRAM
SO_ERROR
查询 socket 错误,一般配合 select error fd 使用,查询完后,errno 的值会被清 0
以下为具体的使用方法和场景。
1 SO_REUSEADDR
:服务器端 socket 关闭后立即复用地址端口
先通过 idf.py menuconfig->component config->lwip->Enable SO_REUSEADDR option
选项使能 SO_REUSEADDR
socket option(大部分 menuconfig 默认使能了此选项),每次创建 socket 后(bind 前)添加以下代码:
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
此时在此 socket 关闭后可立即使用同样的地址端口正常通信。
2 SO_KEEPALIVE
:使能 TCP keepalive 并且设置 keepalive 用于检查僵尸客户端
使能 TCP keepalive 并且设置 keepalive 的参考代码可以参考 这里,如下:
if (cfg->keep_alive_cfg && cfg->keep_alive_cfg->keep_alive_enable) {
int keep_alive_enable = 1;
int keep_alive_idle = cfg->keep_alive_cfg->keep_alive_idle;
int keep_alive_interval = cfg->keep_alive_cfg->keep_alive_interval;
int keep_alive_count = cfg->keep_alive_cfg->keep_alive_count;
ESP_LOGD(TAG, "Enable TCP keep alive. idle: %d, interval: %d, count: %d", keep_alive_idle, keep_alive_interval, keep_alive_count);
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keep_alive_enable, sizeof(keep_alive_enable)) != 0) {
ESP_LOGE(TAG, "Fail to setsockopt SO_KEEPALIVE");
return ESP_ERR_ESP_TLS_SOCKET_SETOPT_FAILED;
}
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &keep_alive_idle, sizeof(keep_alive_idle)) != 0) {
ESP_LOGE(TAG, "Fail to setsockopt TCP_KEEPIDLE");
return ESP_ERR_ESP_TLS_SOCKET_SETOPT_FAILED;
}
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &keep_alive_interval, sizeof(keep_alive_interval)) != 0) {
ESP_LOGE(TAG, "Fail to setsockopt TCP_KEEPINTVL");
return ESP_ERR_ESP_TLS_SOCKET_SETOPT_FAILED;
}
if (setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &keep_alive_count, sizeof(keep_alive_count)) != 0) {
ESP_LOGE(TAG, "Fail to setsockopt TCP_KEEPCNT");
return ESP_ERR_ESP_TLS_SOCKET_SETOPT_FAILED;
}
}
3 SO_LINGER
:强制发送 RST 断开连接
如果关闭 socket,对端不回 FIN 导致 socket 处于 TIME_WAIT 状态, 在 close socket 的地方加 SO_LINGER
socket option 强制发送 RST 断开连接。此时先通过 idf.py menuconfig->component config->lwip->Enable SO_LINGER processing
选项使能 SO_LINGER
socket option,然后在关闭 socket 前添加以下代码:
void onClose(httpd_handle_t hd, int sockfd) {
esp_tls_t *tls = httpd_sess_get_transport_ctx(hd, sockfd);
esp_tls_conn_delete(tls);
struct linger linger;
linger.l_onoff = 1;
linger.l_linger = 0;
if (setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger) == 0) {
ESP_LOGI(TAG, "Set SO_LINGER success");
close(sockfd); // Now call close will send RST to peer and free pcb immediately.
}
ESP_LOGI(TAG, "Closing Socket %d, Heap:%d", sockfd, esp_get_free_heap_size());
}
此时需要注意这里的参数 l_onoff
和 l_linger
:
l_onoff
非 0,l_linger
为 0 时,close()
立刻关闭(RST)l_onoff
非 0,l_linger
非 0 时,close()
会等待l_linger
时间把缓冲区数据发完,到点立刻关闭l_onoff
为 0 时,使用默认方式关闭
注:这部分的参考资料点击 这里。