我注册了哪些网站吗,网络营销策略制定,做淘宝设计能做网站吗,软文范文200字1. 简介 TCP#xff08;Transmission Control Protocol#xff09;#xff0c;全称传输控制协议。它的特点有以下几点#xff1a;面向连接#xff0c;每一个TCP连接只能是点对点的#xff08;一对一#xff09;#xff1b;提供可靠交付服务#xff1b;提供全双工通信Transmission Control Protocol全称传输控制协议。它的特点有以下几点面向连接每一个TCP连接只能是点对点的一对一提供可靠交付服务提供全双工通信面向字节流。
1.1 三次握手 三次握手代表的是TCP的连接过程它表示在成功连接之前服务端和客户端需要通信三次才能最终确认。
第一次握手客户端将标志位SYN置为1随机产生一个值seqJ并将该数据包发送给服务器端客户端进入SYN_SENT状态等待服务器端确认第二次握手服务器端收到数据包后由标志位SYN1知道客户端请求建立连接服务器端将标志位SYN和ACK都置为1ackJ1随机产生一个值seqK并将该数据包发送给客户端以确认连接请求服务器端进入SYN_RCVD状态 第三次握手客户端收到确认后检查ack是否为J1ACK是否为1如果正确则将标志位ACK置为1ackK1并将该数据包发送给服务器端服务器端检查ack是否为K1ACK是否为1如果正确则连接建立成功客户端和服务器端进入ESTABLISHED状态完成三次握手随后客户端与服务器端之间可以开始传输数据了。 1.2 四次挥手 四次挥手代表的是TCP的断开连接过程它表示在成功断开前服务端和客户端需要通信四次才能最终确认。
第一次挥手客户端发送一个FINM用来关闭客户端到服务器端的数据传送客户端进入FIN_WAIT_1状态表示客户端没有数据需要发送了但是如果服务器端还有数据没有发送完成则可以继续发送数据第二次挥手服务器端收到FIN后先发送ackM1告诉客户端请求收到了但是我还没准备好要继续等待我的消息这个时候客户端就进入FIN_WAIT_2 状态继续等待服务器端的FIN报文第三次挥手当服务器端确定数据已发送完成则向客户端发送FINN报文告诉客户端数据发完了准备关闭连接服务器端进入LAST_ACK状态第四次挥手客户端收到FINN报文后就知道可以关闭连接了但是他还是不相信网络怕服务器端不知道要关闭所以发送ackN1后会进入TIME_WAIT状态如果服务端没有收到ACK则可以重传。服务器端收到ACK后就知道可以断开连接了。如果客户端等待了2MSL后依然没有收到回复则证明服务器端已正常关闭那客户端也可以关闭连接了。 1.3 拥塞控制 网络就像我们生活中的交通系统当车流大的时候就可能会导致拥塞TCP为了保证可靠的交付服务所以引入了拥塞控制灵活调整发送策略。 拥塞控制是一个动态的过程它既要提高带宽利用率发送尽量多的数据又要避免网络拥堵丢包RTT增大等问题基于这种高要求并不是单一策略可以搞定的因此TCP的拥塞控制策略实际上是分阶段分策略的综合过程包括慢开始(slow start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery)。
1. 慢开始 慢开始算法的思路为在数据开始发送时由于不清楚网络的负荷情况如果此时立即把大量数据发送到网络那么就有可能引起网络拥塞。根据生活中的经验进行引伸较好的方法是由小大到逐渐增大发送窗口一步步探测网络链路的极限也就是说由小到大逐渐增大拥塞窗口数值cwnd下面是其简要工作流程图。 由上图可见一开始发送方的初始cwnd 为1发送方发送第一个报文段M1并收到接收方的确认。此时发送方将cwnd从1增大到2接着发送M2和M3两个报文段收到两个报文的确认后发送方继续将cwnd加倍增加到4。只要网络仍然通畅那么发送方就会以此类推在每一轮的传输成功后将cwnd进行加倍的操作。 显然cwnd不能无限制地加倍这样会引起网络拥塞因此需要设置一个慢开始门限ssthresh状态变量。当cwnd ssthresh时才会使用上述的慢开始算法。
2. 拥塞避免 当cwnd ssthresh时系统会使用拥塞避免算法该算法的思路是让拥塞窗口cwnd缓慢地增长即每完成一轮传输就把发送方的拥塞窗口cwnd加1而不是像慢开始阶段那样加倍增长。因此在拥塞避免阶段就有“加法增大”(Additive Increase)的特点。这表明在拥塞避免阶段拥塞窗口cwnd按线性规律缓慢增长比慢开始算法的拥塞窗口增长速率缓慢得多。下图展示了拥塞控制的工作流程。 在上图中慢开始门限的初始值为16。一开始系统执行慢开始算法发送方每成功发送一轮报文段就把拥塞窗口值加倍然后开始下一轮的传输。因此拥塞窗口cwnd随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢开始门限值ssthresh 时就开始改为执行拥塞避免算法拥塞窗口按线性规律增长。 当进行到第12轮传输时上图节点2网络出现了超时发送方判断为网络拥塞。于是调整门限值ssthresh cwnd / 2 12同时设置拥塞窗口cwnd 1重新进入慢开始阶段。 上图节点3展示了一个特殊情况此时拥塞窗口cwnd 16这时出现了发送方一连收到3个对同一报文段的重复确认的情况此时执行了拥塞避免算法调整门限值ssthresh cwnd / 2 8。这是因为如果发送方迟迟收不到确认就会产生超时会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始把拥塞窗口cwnd又置为1因而降低了传输效率。
3. 快重传 TCP作为一个可靠的协议面临的很大的问题就是丢包丢包就要重传因此发送方需要根据接收方回复的ACK来确认是否丢包了下图为超时重传的典型时序图。 重传超时时间RTO是随着复杂网络环境而动态变化的在拥塞控制中发生超时重传将会极大拉低cwnd如果网络状况并没有那么多糟糕偶尔出现网络抖动造成丢包或者阻塞也非常常见因此触发的慢启动将降低通信性能故出现了快速重传机制。所谓快速重传时相比超时重传而言的重发等待时间会降低并且后续尽量避免慢启动来保证性能损失在最小的程度下图为其时序图。 快速重传和超时重传的区别在于cwnd在发生拥塞时的取值超时重传会将cwnd修改为最初的值也就是慢启动的值快速重传将cwnd减半二者都将ssthresh设置为cwnd的一半。从二者的区别可以看到快速重传更加主动有利于保证链路的传输性能。
4. 快恢复 在快速重传之后就会进入快速恢复阶段此时的cwnd为上次发生拥塞时的cwnd的1/2之后cwnd再线性增加重复之前的过程。
2. lwIP ESP-IDF使用lwIP库实现TCP/IP协议栈这个库在大多数嵌入式系统中都有用到它是对底层硬件的上层封装所以如果未来要写比如Linux的TCP/IP应用代码也是通用的。
3. 例程 例程分别在ESP32上实现TCP客户端和服务端使用电脑作为另一方进行简单通信测试。需要注意的是测试时ESP32和电脑必须处于同一局域网。 电脑端测试会使用的上位机为野火串口调试助手下载地址FireTools
3.1 客户端 这个例程配置ESP32为客户端当连接WiFi热点成功后会请求连接服务端连接成功后会发送一段消息然后阻塞等待服务端回复服务端恢复消息后ESP32会主动关闭套接字。
#include freertos/FreeRTOS.h
#include freertos/queue.h
#include freertos/semphr.h
#include esp_system.h
#include esp_wifi.h
#include esp_event.h
#include esp_log.h
#include esp_mac.h
#include nvs_flash.h
#include sys/socket.h
#include lwip/err.h
#include lwip/sys.h
#include netdb.h
#include arpa/inet.h#include string.h#define TAG app
#define HOST_IP_ADDR 192.168.10.117
#define HOST_PORT 20001static char rx_buffer[128];
static const char *payload Message from ESP32;
static TaskHandle_t client_task_handle;static void tcp_client_task(void *args)
{struct sockaddr_in dest_addr;inet_pton(AF_INET, HOST_IP_ADDR, dest_addr.sin_addr);dest_addr.sin_family AF_INET;dest_addr.sin_port htons(HOST_PORT);while (1) {int sock socket(AF_INET, SOCK_STREAM, IPPROTO_IP);if (sock 0) {ESP_LOGE(TAG, Unable to create socket: errno %d, errno);break;}ESP_LOGI(TAG, Socket created, connecting to %s:%d, HOST_IP_ADDR, HOST_PORT);int err connect(sock, (struct sockaddr *)dest_addr, sizeof(dest_addr));if (err ! 0) {ESP_LOGE(TAG, Socket unable to connect: errno %d, errno);break;}ESP_LOGI(TAG, Successfully connected);err send(sock, payload, strlen(payload), 0);if (err 0) {ESP_LOGE(TAG, Error occurred during sending: errno %d, errno);break;}memset(rx_buffer, 0, sizeof(rx_buffer));int len recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0);if (len 0) {ESP_LOGE(TAG, recv failed: errno %d, errno);} else {ESP_LOGI(TAG, Received %d bytes from %s:, len, HOST_IP_ADDR);ESP_LOGI(TAG, data: %s, rx_buffer);}close(sock);vTaskDelay(1000 / portTICK_PERIOD_MS);}
}static void wifi_event_handler(void* arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{if (event_base IP_EVENT) {if (event_id IP_EVENT_STA_GOT_IP) {xTaskCreate(tcp_client_task, tcp_client, 2048, NULL, 5, client_task_handle);}} else if (event_base WIFI_EVENT) {if (event_id WIFI_EVENT_STA_DISCONNECTED) {vTaskDelete(client_task_handle);} else if (event_id WIFI_EVENT_STA_START) {esp_wifi_connect();}}
}int app_main()
{/* 初始化NVS */esp_err_t ret nvs_flash_init();if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}/* 初始化WiFi协议栈 */ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(cfg));ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL,NULL));wifi_config_t wifi_config {.sta {.ssid Your SSID,.password Your password,.threshold.authmode WIFI_AUTH_WPA_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, wifi_config));ESP_ERROR_CHECK(esp_wifi_start());return 0;
}ESP32的WiFi驱动初始化在前面的文章已经有详细的介绍了这里不再赘述。 在回调函数中当驱动获取到IP后就会创建TCP客户端的任务。
1. 创建socket套接字 调用socket函数创建第一个参数表示域这里使用IPv4对应IP_INET第二个参数表示socket类型TCP协议只能填SOCK_STREAM第三个参数表示协议栈类型这里填IPPROTO_IP。函数会返回套接字描述符。
2. 连接服务器 调用connect函数第一个参数传入套接字描述符比较重要的是第二个参数要传入服务器的地址信息。结构体的定义如下
struct sockaddr_in {u8_t sin_len;sa_family_t sin_family;in_port_t sin_port;struct in_addr sin_addr;
#define SIN_ZERO_LEN 8char sin_zero[SIN_ZERO_LEN];
};sin_len数据长度一般不需要填sin_family套接字类型IPv4填AF_INETIPv6填AF_INET6其他填AF_UNSPECsin_port端口sin_zero上层预留字节不用管。
3. 发送数据 调用send函数。传入套接字描述符、数据指针和数据长度即可最后一个参数是标志位一般填0即可可选的标志位如下
#define MSG_PEEK 0x01
#define MSG_WAITALL 0x02
#define MSG_OOB 0x04
#define MSG_DONTWAIT 0x08
#define MSG_MORE 0x10
#define MSG_NOSIGNAL 0x20 这些标志位是发送和接收都支持的比较常用的是MSG_DONTWAIT像发送和接收函数是阻塞的使能这个标志位可以让函数立即返回不等待数据。
4. 接收数据 调用recv函数。传入的参数与send函数是一致的不再赘述。
5. 关闭连接 调用close函数。传入套接字描述符即可。 测试的时候先打开上位机设置为TCP服务器填写电脑的IP和端口端口是自定义的但注意不要与原有的端口冲突建议设置20000以上比较保险最后点击开始监听。 3.2 服务端 这个例程就是在ESP32上搭建一个TCP服务器接受局域网中的客户端连接并接收数据。
#include freertos/FreeRTOS.h
#include freertos/queue.h
#include freertos/semphr.h
#include esp_system.h
#include esp_wifi.h
#include esp_event.h
#include esp_log.h
#include esp_mac.h
#include nvs_flash.h
#include sys/socket.h
#include lwip/err.h
#include lwip/sys.h
#include netdb.h
#include arpa/inet.h#include string.h#define TAG app
#define HOST_PORT 20001static const char *payload I have received your message;
static TaskHandle_t server_task_handle;static void tcp_client_task(void *args)
{int *sock args;int len;char rx_buffer[128] {0};while (1) {memset(rx_buffer, 0, sizeof(rx_buffer));len recv(*sock, rx_buffer, sizeof(rx_buffer) - 1, 0);if (len 0) {ESP_LOGE(TAG, Error occurred during receiving: errno %d, errno);} else if (len 0) {ESP_LOGW(TAG, Connection closed);goto __exit;} else {ESP_LOGI(TAG, Received %d bytes, data: %s, len, rx_buffer);send(*sock, payload, strlen(payload), 0);}}__exit:close(*sock);free(sock);vTaskDelete(NULL);
}static void tcp_server_task(void *args)
{esp_ip4_addr_t *ip_addr args;struct sockaddr_in dest_addr {0};dest_addr.sin_addr.s_addr ip_addr-addr;dest_addr.sin_family AF_INET;dest_addr.sin_port htons(HOST_PORT);int sock socket(AF_INET, SOCK_STREAM, IPPROTO_IP);if (sock 0) {ESP_LOGE(TAG, Unable to create socket: errno %d, errno);goto __exit;}int err bind(sock, (struct sockaddr *)dest_addr, sizeof(dest_addr));if (err ! 0) {ESP_LOGE(TAG, Socket unable to bind: errno %d, errno);goto __exit;}err listen(sock, 1);if (err ! 0) {ESP_LOGE(TAG, Error occurred during listen: errno %d, errno);goto __exit;}ESP_LOGI(TAG, Server listen at IPSTR :%d, IP2STR(ip_addr), HOST_PORT);while (1) {struct sockaddr_in source_addr {0};socklen_t addr_len sizeof(struct sockaddr_in);int *client malloc(sizeof(int));*client accept(sock, (struct sockaddr *)source_addr, addr_len);if (*client 0) {ESP_LOGE(TAG, Unable to accept connection: errno %d, errno);} else {ESP_LOGI(TAG, Client IPSTR :%d connected, IP2STR((struct esp_ip4_addr *)source_addr.sin_addr), source_addr.sin_port);xTaskCreate(tcp_client_task, client_task, 2048, client, 6, NULL);}}__exit:close(sock);free(ip_addr);vTaskDelete(NULL);
}static void wifi_event_handler(void* arg,esp_event_base_t event_base,int32_t event_id,void* event_data)
{if (event_base IP_EVENT) {if (event_id IP_EVENT_STA_GOT_IP) {ip_event_got_ip_t *data event_data;esp_ip4_addr_t *ip_addr malloc(sizeof(esp_ip4_addr_t));memcpy(ip_addr, data-ip_info.ip, sizeof(esp_ip4_addr_t));xTaskCreate(tcp_server_task, tcp_server, 2048, ip_addr, 5, server_task_handle);}} else if (event_base WIFI_EVENT) {if (event_id WIFI_EVENT_STA_DISCONNECTED) {vTaskDelete(server_task_handle);} else if (event_id WIFI_EVENT_STA_START) {esp_wifi_connect();}}
}int app_main()
{/* 初始化NVS */esp_err_t ret nvs_flash_init();if (ret ESP_ERR_NVS_NO_FREE_PAGES || ret ESP_ERR_NVS_NEW_VERSION_FOUND) {ESP_ERROR_CHECK(nvs_flash_erase());ESP_ERROR_CHECK(nvs_flash_init());}/* 初始化WiFi协议栈 */ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(cfg));ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL,NULL));ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,ESP_EVENT_ANY_ID,wifi_event_handler,NULL,NULL));wifi_config_t wifi_config {.sta {.ssid Your SSID,.password Your password,.threshold.authmode WIFI_AUTH_WPA_WPA2_PSK,},};ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, wifi_config));ESP_ERROR_CHECK(esp_wifi_start());return 0;
}服务器的代码与客户端是有一部分重合的所以下面只重点介绍不同的地方。
1. 创建套接字 参考上面。
2. 绑定IP与端口 调用bind函数。传入的参数其实跟connect函数是一样的但是这里的IP地址是自己的IP地址端口的话就是自定义的。
3. 监听端口 调用listen函数。第一个参数传入套接字描述符第二个参数用来使能log记录。
4. 接受客户端连接 调用accept函数。传入套接字描述符、IP地址结构体和结构体的长度。这个结构体是用来接收客户端的IP信息的初始化为空即可。这个函数是阻塞的只要没有客户端连接就不会返回如果有客户端连接就会返回该客户端的套接字描述符。 例程中一旦客户端成功连接就会创建一个线程处理这个客户端的数据这样的话就能实现多客户端的连接服务。
5. 接收数据 参考上面。如果recv函数返回0则代表客户端断开了连接这时我们就可以关闭这个套接字退出线程。
6. 发送数据 参考上面。 测试时先将ESP32上电确保服务器已经启动并处于监听状态然后在上位机这里设置为TCP客户端模式填入ESP32的IP和端口然后就可以连接并发送数据了。