在 TCP/IP 協(xié)議中,"IP地址 + TCP或UDP端口號" 可以唯一標(biāo)識網(wǎng)絡(luò)通訊中的一個進(jìn)程,"IP地址+端口號" 就稱為 socket。本文以一個簡單的 TCP 協(xié)議為例,介紹如何創(chuàng)建基于 TCP 協(xié)議的網(wǎng)絡(luò)程序。
創(chuàng)新互聯(lián)建站是一家專注于網(wǎng)站建設(shè)、網(wǎng)站設(shè)計與策劃設(shè)計,金水網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)建站做網(wǎng)站,專注于網(wǎng)站建設(shè)十年,網(wǎng)設(shè)計領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:金水等地區(qū)。金水做網(wǎng)站價格咨詢:028-86922220TCP 協(xié)議通訊流程
下圖描述了 TCP 協(xié)議的通訊流程(此圖來自互聯(lián)網(wǎng)):
下圖則描述 TCP 建立連接的過程(此圖來自互聯(lián)網(wǎng)):
服務(wù)器調(diào)用 socket()、bind()、listen() 函數(shù)完成初始化后,調(diào)用 accept() 阻塞等待,處于監(jiān)聽端口的狀態(tài),客戶端調(diào)用 socket() 初始化后,調(diào)用 connect() 發(fā)出 SYN 段并阻塞等待服務(wù)器應(yīng)答,服務(wù)器應(yīng)答一個SYN-ACK 段,客戶端收到后從 connect() 返回,同時應(yīng)答一個 ACK 段,服務(wù)器收到后從 accept() 返回。
TCP 連接建立后數(shù)據(jù)傳輸?shù)倪^程:
建立連接后,TCP 協(xié)議提供全雙工的通信服務(wù),但是一般的客戶端/服務(wù)器程序的流程是由客戶端主動發(fā)起請求,服務(wù)器被動處理請求,一問一答的方式。因此,服務(wù)器從 accept() 返回后立刻調(diào)用 read(),讀 socket 就像讀管道一樣,如果沒有數(shù)據(jù)到達(dá)就阻塞等待,這時客戶端調(diào)用 write() 發(fā)送請求給服務(wù)器,服務(wù)器收到后從 read() 返回,對客戶端的請求進(jìn)行處理,在此期間客戶端調(diào)用 read() 阻塞等待服務(wù)器的應(yīng)答,服務(wù)器調(diào)用 write() 將處理結(jié)果發(fā)回給客戶端,再次調(diào)用 read() 阻塞等待下一條請求,客戶端收到后從 read() 返回,發(fā)送下一條請求,如此循環(huán)下去。
下圖描述了關(guān)閉 TCP 連接的過程:
如果客戶端沒有更多的請求了,就調(diào)用 close() 關(guān)閉連接,就像寫端關(guān)閉的管道一樣,服務(wù)器的 read() 返回 0,這樣服務(wù)器就知道客戶端關(guān)閉了連接,也調(diào)用 close() 關(guān)閉連接。注意,任何一方調(diào)用 close() 后,連接的兩個傳輸方向都關(guān)閉,不能再發(fā)送數(shù)據(jù)了。如果一方調(diào)用 shutdown() 則連接處于半關(guān)閉狀態(tài),仍可接收對方發(fā)來的數(shù)據(jù)。
在學(xué)習(xí) socket 編程時要注意應(yīng)用程序和 TCP 協(xié)議層是如何交互的:
下面通過一個簡單的 TCP 網(wǎng)絡(luò)程序來理解相關(guān)概念。程序分為服務(wù)器端和客戶端兩部分,它們之間通過 socket 進(jìn)行通信。
服務(wù)器端程序
下面是一個非常簡單的服務(wù)器端程序,它從客戶端讀字符,然后將每個字符轉(zhuǎn)換為大寫并回送給客戶端:
#include#include #include #include #include #define MAXLINE 80 #define SERV_PORT 8000 int main(void) { struct sockaddr_in servaddr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int i, n; // socket() 打開一個網(wǎng)絡(luò)通訊端口,如果成功的話, // 就像 open() 一樣返回一個文件描述符, // 應(yīng)用程序可以像讀寫文件一樣用 read/write 在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù)。 listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); // bind() 的作用是將參數(shù) listenfd 和 servaddr 綁定在一起, // 使 listenfd 這個用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽 servaddr 所描述的地址和端口號。 bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); // listen() 聲明 listenfd 處于監(jiān)聽狀態(tài), // 并且最多允許有 20 個客戶端處于連接待狀態(tài),如果接收到更多的連接請求就忽略。 listen(listenfd, 20); printf("Accepting connections ...\n"); while (1) { cliaddr_len = sizeof(cliaddr); // 典型的服務(wù)器程序可以同時服務(wù)于多個客戶端, // 當(dāng)有客戶端發(fā)起連接時,服務(wù)器調(diào)用的 accept() 返回并接受這個連接, // 如果有大量的客戶端發(fā)起連接而服務(wù)器來不及處理,尚未 accept 的客戶端就處于連接等待狀態(tài)。 connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); n = read(connfd, buf, MAXLINE); printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port)); for (i = 0; i < n; i++) { buf[i] = toupper(buf[i]); } write(connfd, buf, n); close(connfd); } }