TCP / IP的工做html
TCP / IP是Internet上使用的網絡協議。它是協議,ESP32自己自帶了TCP/IP協議,因此,咱們只需瞭解並學會運用便可。編程
首先,有IP地址。這是一個32位值,應該是惟一的每一個設備鏈接到互聯網。一個32位的值能夠被認爲一個的 的四個不一樣的8位值(4-×8 = 32)。因爲咱們能夠表示一個8位的數目爲0到255之間的數值,咱們一般表明與符號的IP地址:服務器
<數字> <數> <數> <數>例如173.194.64.102。網絡
這些IP address不經常使用做的應用程序輸入。取而代之的是文本名稱鍵入如「 google.com , 但不要被誤導,這些名字是在TCP / IP的IL延髓水平。全部的工做都與32位的IP地址有一種映射。socket
須要一個名稱(例如,「 google.com 」)來檢索其對應的IP地址。 該技術,這就是所謂的「域名系統」或DNS。函數
當咱們學習TCP / IP的,其實有三個不一樣的協議在這裏。 第一個是IP(互聯網協議)。這是下面的傳輸層數據報傳遞協議。再其上面的IP層是TCP(傳輸控制協議),其提供的在是無鏈接的IP協議的鏈接。最後是UDP(用戶數據報協議),其在IP協議之上,並提供數據報在應用程序之間(無鏈接)傳輸。當咱們說TCP / IP, 咱們並非說的剛纔講的在IP上運行TCP,但可看出做爲一個核心協議,該協議是IP,TCP和UDP和其餘相關應用水平協議,如DNS,HTTP,FTP,Telnet及更多。post
輕量級IP協議棧 - LWIP學習
若是咱們認爲TCP / IP做爲一種協議,那麼咱們就能夠結束咱們的理解聯網成兩個不一樣的層。google
一個是負責硬件層:從一個地方到另外一個地方得到的1個0的流 。對於常見的實現包括以太網,令牌環...這是由從設備物理線路特色。無線網絡自己就是一個傳輸層。url
一旦咱們能夠發送和接收數據,一個新的水平就在該數據物理網絡上創建起來了,這即是TCP/IP通信,它提供了硬件中的數據傳輸規則,可是TCP / IP是一個大的協議,它包含大量的部件。Espressif中爲咱們綜合了LwIP輕便式通信協議技術以方便開發,提供的LwIP包括下列服務:
• IP
• ICMP
• IGMP
• MLD
• ND
• UDP
• TCP
• sockets API
• DNS
TCP
TCP鏈接,經過該協議數據能夠在兩個方向上流動,在鏈接創建以前,它是被動監聽傳入的鏈接請求。鏈接的另外一方負責啓動鏈接,它主動請求鏈接造成。一旦鏈接造成,兩邊均可以發送和接受數據,爲了「客戶端」請求鏈接,它必須知道的地址信息,供服務器監聽。 這個地址有兩個不一樣的部分。第一部分是服務器是IP地址和第二部分是特定的「端口號」。咱們沒法看到一個ESP32如何設置本身爲一個偵聽傳入的TCP / IP鏈接,這就要求咱們開始瞭解的重要socket API
TCP鏈接過程
TCP鏈接過程須要三次交互才能完成,以下圖所示:
首先客戶端向服務器發售那個一個SYN報文段指明客戶端打算鏈接的服務器端口,以及出生序號,服務器發回包含服務器初始序號的SYN報文段做爲應答,接着,客戶端對服務器的SYN報文段進行確認。這三次報文段完成鏈接的過程,稱爲三次握手。
TCP關閉過程
終止一個鏈接須要四次握手,以下圖所示
產生四次握手的緣由是因爲TCP的半關閉形成的,既然一個TCP鏈接是全雙工的,那麼每一個方向必須單獨的進行關閉,原則就是當一方完成它的數據發送過程後就能發送一個FIN來終止這個方向鏈接,當一端收到一個FIN,他必須通知應用層另外一端已經終止了哪一個方向的數據傳送。發送FIN一般是應用層進行關閉的結果,收到一個FIN只意味着這一方向沒有數據流動,一個TCP鏈接在收到一個FIN後仍能發送數據。
TCP/IP Sockets(對於詳細的socker API可看個人這篇隨筆: SOCKET API)
TCP/IP socker API是一個編程接口,它是網絡編程中最重要的API,其根據不一樣模式的編程風格不一樣:
對於TCP服務器是經過創建:
1.建立TCP套接字
2.關聯本地端口與插座
3.設置 套接字監聽模式
4.接受來自客戶端的新鏈接
5.接收和發送數據
6.關閉客戶機/服務器鏈接
7.返回到步驟4
對於TCP客戶端創建:
1.建立TCP套接字
2.鏈接到TCP服務器
3.發送數據/接收數據
4.關閉鏈接
其編程模型以下所示:
socket API頭定義中能夠找到 <LWIP / sockets.h> 。對於客戶端和服務器,建立套接字的任務是同樣的,調用
int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
返回 的sock 是用來指向套接字的整數句柄。
當咱們建立了一個服務器端的socket,咱們但願它監聽傳入鏈接要求。要作到這一點,咱們須要告訴socket 哪一個TCP/IP端口他須要監聽(注意,咱們並不提供端口類型是int仍是short),咱們經過調用htons()函數提供類型,它的功能是將數據轉換爲咱們的網絡字節順序,在互聯網上多字節的二進制數據實際是「大端」的格式,如9876(Decima的 升),那麼它以二進制表示爲00100110 10010100或0x26D4的十六進制。對於網絡字節傳輸順序,咱們首先傳送10010100(0xD4),再傳輸00100110(0×26),而ESP32是一個 小端機體系結構,這意味着咱們必須改造2字節和4張字節數爲網絡字節順序(big endian)的。
在給定的裝置中,在一個時間只有一個應用程序可使用給定的本地端口,若是咱們想端口關聯與應用,咱們能夠調用bind()函數來完成,下面給出一個實例:
struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(portNumber); bind(sock, (struct sockaddr *)&serverAddress, sizeof(serverAddress));
如今socket已經和相關的接口鏈接起來了,咱們下一步就要開始調用listen()函數來監聽輸入的數據,listen() 接口函數看到以下:
listen(sock, backlog)
這裏的backlog是,當咱們監聽的接口上esp32發生屢次請求時,因爲沒法當即處理便會產生backlog(積壓),這裏是對積壓值進行設定,當發送請求的數目大於誰的那個的backlog數時,ESP32便不會將這個請求放入積壓隊列中,而是馬上拒絕這個請求,這樣不只防止了是空的資源消耗再服務器上,也能夠做爲指示給調用者。從服務器的角度來看,咱們還須要作一些 工做,當服務器正在處理一個客戶端的請求時,此時另一個客戶端也發清了對端口的請求,此時,accept() API便可調用解決這個問題,當accept()被調用時,下面兩中狀況中的一種可能發生:若是沒有客戶端鏈接等待者,咱們將阻塞等待直到客戶端鏈接到來。另外一種狀況是,若是已經有一個客戶端在那等待連接了,咱們將馬上處理鏈接。這輛中狀況的區別在於咱們是否須要等待鏈接到來。
API調用示例以下:
struct sockaddr_in clientAddress; socklen_t clientAddressLength = sizeof(clientAddress); int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength);
須要關注的是,從accpt()返回的是一個新的socket(整數句柄)。
和全部的TCP鏈接是類似的,鏈接是對稱和雙向的,這意味着,再也不具備客戶端服務器的概念,雙方均可以發送和接收,不一樣的是,咱們沒有必要調用bind()/listen()/accept()
struct sockaddr_in clientAddress; socklen_t clientAddressLength = sizeof(clientAddress); int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength);
SOCKET系列函數
一、socket函數:函數功能是打開網絡通訊接口,爲了執行I/O操做,第一件要作的事情是調用socket函數,socket函數的原型以下:
socket(int falmily, int type, int protocol);
這裏family指明協議簇,取值如表所示:
一般狀況下咱們都是用AF_INET,可是IPv6大規模的普及,AF_INET6取值也會普遍用到,在某些程序中,可能還會看到PF_INET等以PF爲前綴的宏,最先的時候定義AF_表示地址簇,PF表示協議簇,可是如今PF已經不多使用了。
type指明瞭套接字的類型,取值以下表:
一般使用SOCK_STREAM 和SOCK_DRGRAM取值,當使用TCP或者SCTP時,就取SOCK_STREAM,當使用UDP時就用SOCK_DGRAM。
protocol參數指明協議類型,取值以下表所示:
socket函數在成功時返回一個小的非負整數,他和文件描述符相似,咱們稱爲套接字描述符。
二、connect函數:TCP客戶端用來和TCP服務器創建鏈接的,原型以下:
int connect(int sockfd, const struct sockaddr *servaddr,socklen_t addrlen);
sockfd參數是由socket函數返回的套接字描述符。
aervaddr參數是須要鏈接的遠端的服務器的地址信息。
addrlen參數則是servaddr的字節大小。
connect()鏈接成功返回0,出錯返回-1,返回-1後能夠獲取錯誤碼獲得具體的失敗的緣由。當TCP調用connect函數後,就會觸發一個三次握手過程,這裏強調TCP的元嬰是由於使用UDP的時候也能夠調用connect函數。
三、bind函數:把一個本地協議地址賦予一個套接字,更簡單點來講,就是將本地IP地址和端口與套接字綁定在一塊兒。
bind函數原型以下:
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
sockfd參數是由socket函數返回的套接字描述符。
myaddr參數是本地的須要綁定的地址信息。
addrlen參數則是myaddr的字節大小。
bind()成功返回0,失敗返回-1。
bind函數會應爲myaddr參數設置不一樣狀況,表現出不一樣的行爲:
1)TCP服務器端。服務器端不指定端口的狀況很是少見,應爲客戶端須要知道服務器的端口號,若是不指定的話,內核會指定一個臨時端口,經過一些其餘措施來通知客戶端本身的端口號,若是不指定的話,內核會指定一個臨時端口,在RPC(Remote Procedure Call,遠過程調用)服務器中就會不指定端口,經過一些其餘措施來通知客戶端本身的端口,服務器端不指定地址的話,內核就會把客戶端發送給SYN時攜帶的目的IP地址做爲服務器的源地址。若是制定IP地址的話,那麼服務器就只接收這個IP地址的數據。
2)TCP客戶端。客戶端通常不須要調用bind函數,在這種狀況下,內核會根據外出接口綁定一個IP地址,並臨時指定一個端口。若是調用bind函數的話,那麼就會使用制定的IP或者端口。
3)UDP服務器端:服務器端不指定IP地址,套接口會接收到達它綁定端口的任何UDP數據報。並以數據報的外出接口的主IP地址爲源IP地址,以接收到的源IP地址做爲它的目的IP地址發回應答。當指定定本機IP地址,這就限制了套接口只接收到達它綁定端口而且目的地址爲此IP地址的UDP數據報。並以綁定的IP地址做爲源IP地址,以接收的源IP地址做爲它的目的IP地址發回應答。
4)UDP客戶端。和TCP客戶端的行爲相似,若UDP客戶端未綁定IP地址,當它調用sendto時內核會根據外出接口給它綁定一個IP地址和一個臨時端口號,若UDP客戶端綁定了IP地址,他就爲發出的數據報指定了一個源IP地址,而且UDP服務器在接到這個數據報後會以這個IP地址做爲迴應數據報的目的IP地址。
對於不指定地址的狀況,咱們稱之爲通配地址,使用常量INADDR_ANY表示,這個值一般也是0,在不指定端口的狀況,就是端口爲0,下表爲這幾種組合的狀況
bind地址組合
四、listen函數:僅在TCP服務器調用,監聽客戶發起的connect,若是監聽到客戶的connect,則和額客戶進行三次握手,listen函數的原型以下:
int listen(int sockfd, int backlog);
sockfd參數是由socket函數返回的套接字描述符。
backlog參數表示最多容許有backlog個客戶端處於鏈接等待狀態,若是接收到更多的鏈接請求就忽略。
listen()成功返回0,失敗返回-1.
五、accept函數:當完成三次握手後,接受這個鏈接,從未完成鏈接隊列轉移到已完成鏈接隊列。accept函數原型以下:
int accept(int socket, struct sockaddr *cliaddrm, socklen_t *addrlen);
、 sockfd參數是由socket函數返回的套接字。
cliaddr參數是一個傳出參數,accept()返回時傳出客戶端的地址和端口號。
addrlen參數是一個傳入傳出參數,傳入的是調用者提供的緩衝去cliaddr的長度,以免緩衝區溢出問題,傳出的是客戶端地址結構體的實際長度,若是給cliaddr參數傳NULL,表示不關心客戶端的地址。
accept()的返回值是另一個文件描述符connfd,以後與客戶端之間就經過connfd通訊,最後關閉connfd斷開鏈接,而不關閉listenfd。accept()成功返回一個文件描述符,出錯返回-1。
六、recv和send函數:recv函數和send函數分別是接收和發送數據的函數,在有些地方一般也使用read和write來代替這兩個函數。recv和send函數的原型以下;
SSIZE_T recv(int sockfd, void *buff, size_t nbytes, int flags); SSIZE_T send(int sockfd, const void *buff, size_t nbytes, int flags);
sockfd參數對於recv來講就是接收端的描述符,對於send來講就是發送端的描述符。若是是服務器端,就是accept返回的描述符,若是是客戶端時,就是socket返回的描述符。
buff參數是數據緩衝去,對recv來講就是接收數據的緩衝區,對於send來講就是發送數據的緩衝區。
nbytes參數是緩衝去的字節大小。
flags參數是一些手法的特殊標記,值通常爲0或下表的取值。
收發特殊標記
recv和send函數若是成功都會返回接收或者發送的數據字節數,不然返回-1。
1) recv先等待s的發送緩衝區的數據被協議傳送完畢,若是協議在傳送sock的發送緩衝區中的數據時出現網絡錯誤,那麼recv函數返回SOCKET_ERROR
2) 若是套接字sockfd的發送緩衝區中沒有數據或者數據被協議成功發送完畢後,recv先檢查套接字sockfd的接收緩衝區,若是sockfd的接收緩衝區中沒有數據或者協議正在接收數據,那麼recv就一塊兒等待,直到把數據接收完畢。當協議把數據接收完畢,recv函數就把s的接收緩衝區中的數據copy到buff中(注意協議接收到的數據可能大於buff的長度,因此在這種狀況下要調用幾回recv函數才能把sockfd的接收緩衝區中的數據copy完。recv函數僅僅是copy數據,真正的接收數據是協議來完成的)
3) recv函數返回其實際copy的字節數,若是recv在copy時出錯,那麼它返回SOCKET_ERROR。若是recv函數在等待協議接收數據時網絡中斷了,那麼它返回0。
4) 在unix系統下,若是recv函數在等待協議接收數據時網絡斷開了,那麼調用 recv的進程會接收到一個SIGPIPE信號,進程對該信號的默認處理是進程終止。
1) send先比較發送數據的長度nbytes和套接字sockfd的發送緩衝區的長度,若是nbytes > 套接字sockfd的發送緩衝區的長度, 該函數返回SOCKET_ERROR;
2) 若是nbtyes <= 套接字sockfd的發送緩衝區的長度,那麼send先檢查協議是否正在發送sockfd的發送緩衝區中的數據,若是是就等待協議把數據發送完,若是協議尚未開始發送sockfd的發送緩衝區中的數據或者sockfd的發送緩衝區中沒有數據,那麼send就比較sockfd的發送緩衝區的剩餘空間和nbytes
3) 若是 nbytes > 套接字sockfd的發送緩衝區剩餘空間的長度,send就一塊兒等待協議把套接字sockfd的發送緩衝區中的數據發送完
4) 若是 nbytes < 套接字sockfd的發送緩衝區剩餘空間大小,send就僅僅把buf中的數據copy到剩餘空間裏(注意並非send把套接字sockfd的發送緩衝區中的數據傳到鏈接的另外一端的,而是協議傳送的,send僅僅是把buf中的數據copy到套接字sockfd的發送緩衝區的剩餘空間裏)。
5) 若是send函數copy成功,就返回實際copy的字節數,若是send在copy數據時出現錯誤,那麼send就返回SOCKET_ERROR; 若是在等待協議傳送數據時網絡斷開,send函數也返回SOCKET_ERROR。
6) send函數把buff中的數據成功copy到sockfd的改善緩衝區的剩餘空間後它就返回了,可是此時這些數據並不必定立刻被傳到鏈接的另外一端。若是協議在後續的傳送過程當中出現網絡錯誤的話,那麼下一個socket函數就會返回SOCKET_ERROR。(每個除send的socket函數在執行的最開始總要先等待套接字的發送緩衝區中的數據被協議傳遞完畢才能繼續,若是在等待時出現網絡錯誤那麼該socket函數就返回SOCKET_ERROR)
7) 在unix系統下,若是send在等待協議傳送數據時網絡斷開,調用send的進程會接收到一個SIGPIPE信號,進程對該信號的處理是進程終止。
六、close函數:用來關閉socket,而且終止TCP鏈接。其函數原型以下:
int close(int sockfd);
參數sockfd就是須要關閉的套接字的描述符。
close函數默認行爲是吧套接字標記爲已關閉,而後當即返回調用進程。此時,調用進程中將不能再使用該描述符。值得注意的是,函數是當即返回,其中的含義就是TCP鏈接並非當即被終止,也就是說,儘管close函數已經返回,可是TCP協議還在工做,還要嘗試將在緩衝區中未發送的數據發送到對端,而後在進行四次交互的關閉流程。對於這種行爲可使用套接字選項SO_LINGER來改變。
七、shutdown函數:shutdown也是關閉socket,而且終止TCP鏈接,一般狀況況下都會使用close函數進行關閉,但某些狀況下也可使用shutdown函數。shutdown函數的原型以下:
int shutdown(int sockfd, int howto);
參數sockfd就是須要關閉的套接字的描述符。
參數howto表示關閉選項。選項值以下:
SHUT_RD,取值爲0,表示關閉鏈接的杜這半部;SHUT_WR,取值爲1,表示關閉鏈接的寫這半部;SHUT_RDWR,取值爲2,表示關閉鏈接的讀這半部和寫這半部;當參數取2時的效果和連續調用兩次shutdown函數分別取0和1的效果相同。這個涉及TCP的半關閉概念,詳情能夠查看查理。史蒂芬文斯的《TCP-IP詳解卷 I:協議》。
SOCKET中的地址轉換
sockaddr_in,sockaddr,in_addr在socket中都有應用,這裏來對它們進行區分
sockaddr和sockaddr_in在字節長度上都爲16個BYTE,能夠進行轉換
struct sockaddr { unsigned short sa_family; //2 char sa_data[14]; //14 };
上面是通用的socket地址,具體到Internet socket,用下面的結構,兩者能夠進行類型轉換
struct sockaddr_in { short int sin_family; //2 unsigned short int sin_port; //2 struct in_addr sin_addr; ‘//4 unsigned char sin_zero[8]; //8 }; struct in_addr就是32位IP地址。 struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; #define s_addr S_un.S_addr };
或者;
struct in_addr { in_addr_t s_addr; };
結構體in_addr 用來表示一個32位的IPv4地址
inet_addr()是將一個點分制的IP地址(如192.168.0.1)轉換爲上述結構中須要的32位二進制方式的IP地址(0xC0A80001)。//server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
一般的作法是:填值的時候使用sockaddr_in結構,而做爲函數(如bin, accept, connect等)的參數傳入的時候轉換成sockaddr結構就好了,畢竟都是16個字符長。
一般的用法是:
int sockfd; struct sockaddr_in my_addr; //賦值時用這個結構 sockfd = socket(AF_INET, SOCK_STREAM, 0); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(MYPORT); my_addr.sin_addr.s_addr = inet_addr("192.168.0.1"); bzero(&(my_addr.sin_zero), 8); bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));//用(struct sockaddr *)轉換即知足要求 //int accept(int s,struct sockaddr * addr,int * addrlen);//這三個函數的第二個參數結構都爲struct sockaddr,因此通常作法都如上所示。 //int bind(int sockfd,struct sockaddr * my_addr,int addrlen); //int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
struct sockaddr 是一個通用地址結構,這是爲了統一地址結構的表示方法,統一接口函數,使不一樣的地址結構能夠被bind() , connect() 等函數調用;struct sockaddr_in中的in 表示internet,就是網絡地址,這只是咱們比較經常使用的地址結構,屬於AF_INET地址族,他很是的經常使用,以致於咱們都開始討論它與 struct sockaddr通用地址結構的區別。另外還有struct sockaddr_un 地址結構,咱們能夠認爲 struct sockaddr_in 和 struct sockaddr_un 是 struct sockaddr 的子集。
到這裏,SOCKET編程的基本函數就介紹完成了,下篇文章: