前言
??關于計網的什么TCP三次握手 幾層模型啊TCP報文啥的不在這里講,會單獨分成一個計算機網絡模塊
??這里主要介紹介紹lwip和socket
FreeRTOS下的網絡接口–移植LWIP
?? 實際上FreeRTOS并不自帶網絡接口,我們一般會通過移植lwip協議棧讓FreeRTOS可以通過網絡接口收發數據,具體可看博客:
一文帶你掌握LWIP
- LWIP是什么
??LWIP是一個在嵌入式領域應用的TCP/IP協議棧,除了TCP/IP外還能支持DNS,DHCP等應用。LWIP只需要十幾KB的RAM和幾十KB的ROM就能使用了 - 如何在RTOS移植LWIP
?? 移植lwip前 結合著OSI模型先來說說LWIP幫我們做了哪些工作
?? 當我們的應用想要發起數據傳輸的時候,LWIP幫我們完成了TCP報文封裝(傳輸層)–>IP報文封裝(網絡層)–>IP地址找到MAC地址以及對應封裝(APR協議–數據鏈路層) 我們需要做的就是把這個層層封裝好的報文(p_buf鏈表)通過我們實現的網絡驅動接口發送出去
- step1 :編寫 sys_arch.c文件
??首先我們的lwip在OS下至少需要三種東西:消息郵箱/信號量/線程創建
?? ??可是問題是,如果我用FreeRTOS,這三東西是這些API,我用UCOSIII又是一套API,這可怎么辦呢? 那lwip就把這些所有需要的操作抽象出來,然后根據不同的RTOS環境填空就好,這就是sys_arch.c做的工作,我們要去自己寫sys_arch的API
err_t
sys_mutex_new(sys_mutex_t *mutex)
{LWIP_ASSERT("mutex != NULL", mutex != NULL);mutex->mut = xSemaphoreCreateRecursiveMutex();if(mutex->mut == NULL) {SYS_STATS_INC(mutex.err);return ERR_MEM;}SYS_STATS_INC_USED(mutex);return ERR_OK;
}
void
sys_mutex_lock(sys_mutex_t *mutex)
{BaseType_t ret;LWIP_ASSERT("mutex != NULL", mutex != NULL);LWIP_ASSERT("mutex->mut != NULL", mutex->mut != NULL);ret = xSemaphoreTakeRecursive(mutex->mut, portMAX_DELAY);LWIP_ASSERT("failed to take the mutex", ret == pdTRUE);
}
err_t
sys_sem_new(sys_sem_t *sem, u8_t initial_count)
{LWIP_ASSERT("sem != NULL", sem != NULL);LWIP_ASSERT("initial_count invalid (not 0 or 1)",(initial_count == 0) || (initial_count == 1));sem->sem = xSemaphoreCreateBinary();if(sem->sem == NULL) {SYS_STATS_INC(sem.err);return ERR_MEM;}SYS_STATS_INC_USED(sem);if(initial_count == 1) {BaseType_t ret = xSemaphoreGive(sem->sem);LWIP_ASSERT("sys_sem_new: initial give failed", ret == pdTRUE);}return ERR_OK;
}
- step2: 實現底層網卡驅動程序
這個就得我們根據硬件自己編寫了 - step3: 分配/設置/注冊一個netif結構體
netif結構體是吧我們的網卡驅動程序和lwip鏈接起來的關鍵,netif結構體中包括數據的發送函數等struct netif {struct netif *next; // 以鏈表形式方便管理ip_addr_t ip_addr; // 本地ip地址ip_addr_t netmask; // 子網掩碼ip_addr_t gw; // 網關netif_output_fn output; // 供IP層封裝完成后調用 一般就用 etharp_output()netif_linkoutput_fn linkoutput; // ethernet_output()結束封裝包后調用, 用于發送數據包netif_input_fn input; // 用于向上層協議提交數據包// 以下是各種call_back沒用上 直接不展示了netif_status_callback_fn status_callback;.....u16_t mtu; // 最大傳輸字節 mtu = 1500一般u8_t hwaddr[NETIF_MAX_HWADDR_LEN]; // mac地址u8_t hwaddr_len; // mac地址長度u8_t flags; // 網卡的狀態void * state; // 私有數據 看自己怎么用 };
??我們需要配置好這些參數的內容 然后通過netif_setup來使能這個網卡
?? ??為什么會有多個netif?–IP協議會根據ip_route函數去找到最合適的netif把數據發送出去,不過一般來說只有一個網卡啦
具體如何初始化這個網卡的,可以看我上面提到的博客
- step4: 初始化LWIP的核心線程
tcpip_init()函數 - step5: 配置lwip協議棧 lwip的參數(lwipopts.h )
- LWIP數據接收/發送過程?
接收過程: 底層網卡通過DMA/中斷收到數據–>把數據轉成p_buf結構體–>調用netif->input提交給上層協議棧–>LWIP的核心線程會來處理這個數據的
發送過程: 應用層發起操作–>TCP協議封包–>IP協議封包并找到最合適的netif結構體–>ARP協議封包–>底層網卡驅動把數據發送輸出
4. LWIP參數配置?–lwipopts.h
根據自己的實際需求去配置了
比如是否啟用哪些協議 / 堆棧內存的大小 / 是否需要硬件校驗
5. LWIP的幾種API
LWIP有RAW API / NETCOON API / SOCKET API三種
-
LWIP的內存管理?
??LWIP提供了兩種內存管理方式: 堆內存管理和內存池內存管理 這倆中內存管理方式是可以共存的,也可以強行只用一種—(忽略標準庫的malloc和free)
內存池的使用范圍:固定大小的場景,比如TCP/IP的首部用內存池就更快
- 內存池的定義:實際上就是一個大數組–通過DECRLAR宏定義
堆內存管理的使用: 靈活的大小,比如我們的數據包大小就是不確定的 通過堆內存管理算法分配–
-
內存堆的定義:實際上也是一個大數組–通過DECRLAR宏定義
-
如何兩者都啟用(默認就是)?或者只啟用一種
- 內存池的定義:實際上就是一個大數組–通過DECRLAR宏定義
Linux下的網絡接口–Socket
- 請說一下socket網絡編程中客戶端和服務端用到哪些函數?
- TCP服務器(Server)
- 使用函數socket()創建一個socket
int socket(int domain, int type, int protocol);
- 設置端口復用(可選):允許多個進程或線程共享同一端口號進行通信的技術 — 提高服務器并發能力,防止端口資源耗盡
- 使用函數bind()綁定IP地址,端口等信息到socket上,設置全通規則
struct sockaddr_in serv_addr;serv_addr.sin_family = AF_INET;serv_addr.sin_port = htons(8080);serv_addr.sin_addr.s_addr = INADDR_ANY;bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); // 綁定IP地址和端口號
- 使用函數listen()設置監聽,使用函數accept()接收客戶端上來的連接
int listen(int sockfd, int backlog); //backlog等待隊列的長度
- 使用函數send()和recv(),或者read()和write()收發數據
ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 關閉網絡連接
- TCP客戶端(Client)
- 使用函數socket()創建一個socket
- 使用函數connect()連接服務器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 使用函數send()和recv(),或者read()和write()收發數據
- 關閉網絡連接
UDP是基于無連接的協議,發送數據時不需要先建立連接,而是直接把數據發送過去
- UDP服務器(Server)
- 使用函數socket()創建一個socket
- 使用函數bind() 綁定IP地址、端口等信息到socket上
- 收發數據,用函數recvfrom(),sendto()
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
- 關閉網絡連接close()
- UDP客戶端(Client)
- 使用函數socket()創建一個socket
- 使用函數recvfrom(),sendto()收發數據
- 關閉網絡連接close()
- TCP服務器(Server)
- 網絡字節序是大小端?
- 大端字節序(Big Endian):最高有效位存于最低內存地址處,最低有效位存于最高內存處;
- 小端字節序(Little Endian):最高有效位存于最高內存地址,最低有效位存于最低內存處
網絡字節序時大端字節序
//將主機字節序轉換為網絡字節序
unit32_t htonl (unit32_t hostlong);
unit16_t htons (unit16_t hostshort);
//將網絡字節序轉換為主機字節序
unit32_t ntohl (unit32_t netlong);
unit16_t ntohs (unit16_t netshort);
- 為什么在數據結構 struct sockaddr_in 中, sin_addr 和 sin_port 需要轉換為網絡字節順序,而sin_family 需不需要呢?
sin_addr 和 sin_port 分別封裝在包的 IP 和 UDP 層。因此,它們必須要 是網絡字節順序。但是 sin_family 域只是被內核 (kernel) 使用來決定在數 據結構中包含什么類型的地址,所以它必須是本機字節順序。同時, sin_family 沒有發送到網絡上,它們可以是本機字節順序
3 Socket的阻塞和非阻塞模式
- 阻塞模式
調用 send()/recv() 時,若數據未就緒或緩沖區滿,線程會掛起,直到操作完成 - 非阻塞模式
調用 send()/recv() 立即返回,通過錯誤碼(如 EWOULDBLOCK)通知需重試
需配合 ?I/O 多路復用?(如 select()/poll()/epoll)實現高效事件驅動- 非阻塞下的Socket
在非阻塞模式下,connect() 會立即返回 EINPROGRESS(而不會等三次握手完成再返回),此時需通過 select/poll 監聽 Socket 的可寫事件,再通過 getsockopt(SO_ERROR) 檢查連接是否成功。關鍵點包括:嚴格錯誤檢查、超時控制、與非阻塞 IO 的協同處理
- 非阻塞下的Socket