這篇文章將會編寫基本的服務器網絡程序,主要講解服務器端和客戶端代碼的原理,至于網絡名詞很具體的概念,例如什么是TCP協議,不會過多涉及。
首先介紹一下TCP網絡編程的兩種模式:服務器端和客戶端模式:
????????首先說明一下:黑色線代表狀態的轉換,紅色線表示的是數據的傳輸,read 和 write 之間的循環表示:例如讀取完數據,進入寫入的狀態,寫入完再進入讀取的狀態,一直循環,實現了服務器和客戶端之間的通信。
首先來解釋一下服務器端:
int socket(int domain, int type, int protocol)
socket()?表示創建一個套接字。套接字是網絡通信的基本數據結構,用于定義通信協議(如 TCP 或 UDP)和地址族(如 IPv4 或 IPv6)。通過套接字,服務器和客戶端可以在網絡上傳輸數據,可以把套接字理解為一個編程接口,利用套接字實現程序和網絡的連接,像是用戶層和傳輸層(TCP)中間的一個抽象層,有了套接字才可以向網絡發送數據。
傳入的內容是(協議族,套接字類型,默認協議(通常為0))
返回:成功返回套接字描述符,失敗返回-1?
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
bind() 表示將套接字綁定到一個特定的地址和端口。綁定的地址和端口標識服務器,使客戶端能夠找到并連接到該服務。只有套接字還不夠,我還要知道是哪個主機(IP)發送的,哪個應用程序(端口)發送的,端口可以理解為電腦通信的入口和出口。
傳入的內容是:(套接字描述符,地址結構體的地址,地址結構體大小)
返回:成功返回0,失敗返回-1。
int listen(int sockfd, int backlog)
listen()?表示將套接字轉換為監聽模式,并設置等待連接的隊列長度。當多個客戶端請求連接時,服務器會將這些請求加入隊列,按順序處理。
傳入的內容是(套接字描述符,隊列的長度)
返回:成功返回0,失敗返回-1。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept()?表示等待接受客戶端的連接請求,接收到請求,成功連接后,accept()?返回一個新的套接字,用于與該客戶端通信,而原始監聽套接字則繼續處理新的連接請求。
傳入的內容是:(套接字描述符,地址結構體的地址,地址結構體大小的地址)
返回:成功返回新的套接字描述符,失敗返回-1?
ssize_t read(int sockfd, void *buf, size_t count)
read()?表示從套接字描述符中讀取數據,用于接收客戶端發送的消息。讀取的數據存儲在提供的緩沖區中。
傳入的內容是:(套接字描述符,緩沖區指針(數組),要讀取的字節數)
返回:成功返回實際讀取的字節數,失敗返回-1。
ssize_t write(int sockfd, const void *buf, size_t count)
write()?表示向套接字描述符中寫入數據,用于向客戶端發送響應數據。
傳入的內容是:(套接字描述符,緩沖區指針(數組),要寫入的字節數)
返回:成功返回實際寫入的字節數,失敗返回-1。
int close(int sockfd)
close()表示關閉套接字描述符。
傳入的內容是:(套接字描述符)
返回:成功返回0,失敗返回-1。?
接著解釋一下客戶端的新出現的函數:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connet()表示客戶端向服務器發起連接請求。客戶端告訴操作系統需要連接到哪個服務器的哪個端口。
傳入的內容是:(套接字描述符,地址結構體的地址,地址結構體大小)
返回:成功返回0,失敗返回-1。
看完這些,你會發現:套接字描述符和文件描述符很像,都可以根據描述進行寫入讀取和各種其他操作,其實,這就是UNIX系統和類UNIX系統(Linux系統)的抽象資源管理方式,通過整數來標識系統中的資源,使用統一的接口設計,“一切皆文件”。
看到這里,你一定有幾個問題:
1.為什么客戶端少了bind()和listen()的操作?
2.為什么connect操作指向了accept操作之后?
3.地址結構體的地址addr是個什么東西?
4.為什么有的函數傳addr大小,有的傳addr大小的地址?
1.對于服務器端來說,服務器需要綁定到固定的端口這樣客戶端才能知道它,對于客戶端來說,操作系統會在必要的時候分配臨時的本地端口和地址,不需要再綁定端口。
2.因為服務器端的accept函數是阻塞的,等待客戶端發起請求,當connect發送給服務器端請求之后,才會繼續進行后面的讀寫操作。
3.addr的類型如下:有兩個成員,分別是地址族,地址和端口信息,但是這不方便我們進行設置,所以一般采用 sockaddr_in 這個結構,最后在進行強制類型轉換得到sockaddr,注意這兩個結構體類型大小是一樣的,只是結構不一樣。
struct sockaddr {sa_family_t sa_family; // 地址族,例如 AF_INET(IPv4)或 AF_INET6(IPv6)char sa_data[14]; // 地址和端口信息
};
下面是sockaddr_in結構體類型,可以清楚地看到每個成員的含義:
struct sockaddr_in {sa_family_t sin_family; // 地址族,通常為 AF_INET(IPv4)uint16_t sin_port; // 端口號(16 位),以網絡字節序表示struct in_addr sin_addr; // IP 地址(32 位)char sin_zero[8]; // 保留字段,填充用
};
4.可以看到accept函數的addrlen參數是 addr 大小(變量)的地址,但是connect和bind函數的addrlen參數是 addr 大小(變量)本身,這是因為accept不知道調用者提供的 addr 緩沖區的大小可能是IPv4可能是IPv6,所以需要地址地址。
我猜測可能和TCP的三次握手或者accept返回新的套接字或者客戶端分配動態端口有關系,而connect和bind函數都是用已知的套接字進行操作,所以不會進行addr大小的改變,所以可以直接傳值。
這就是TCP編程的兩種模式,從下篇文章開始,我們將學習如何編寫服務器端和客戶端的代碼。
這就是文章的所有內容了,希望對你有所幫助,如有錯誤歡迎指出。