第二章
編寫套接字服務器的步驟
套接字服務器比客戶機稍微複雜一點,這主要是因爲服務器通常需要能夠處理多個客戶機請求。服務器基本上包括兩個方面:處理每一個已建立的連接,以及要建立的連接。
在我們的例子中,以及在大多數情況下,都可以將特定連接的處理劃分爲支持函數,這看起來有點像 TCP 客戶機所做的事情。我們將這個函數命名爲 HandleClient()。
對新連接的監聽與客戶機有一點不同,其訣竅在於,最初創建並綁定到某個地址或端口的套接字並不 是實際連接的套接字。這個最初的套接字的作用更像一個套接字工廠,它根據需要產生新的已連接的套接字。這種安排在支持派生的、線程化的或異步的分派處理程 序(使用 select())函數)方面具有優勢;不過對於這個入門級的教程,我們將僅按同步的順序處理未決的已連接套接字。
TCP 回顯服務器(應用程序設置)
我們的回顯服務器與客戶機非常類似,都以幾個 #include 語句開始,並且定義了一些常量和錯誤處理函數:
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define MAXPENDING 5 /* Max connection requests */
#define BUFFSIZE 32
void Die(char *mess) { perror(mess); exit(1); }
常量 BUFFSIZE 限定了每次循環所發送的數據量。常量 MAXPENDING 限定了在某一時間將要排隊等候的連接的數量(在我們的簡單的服務器中,一次僅提供一個連接服務)。函數 Die() 與客戶機中的相同。
TCP 回顯服務器(連接處理程序)
用於回顯連接的處理器程序很簡單。它所做的工作就是接收任何可用的初始字節,然後循環發回數據並接收更多的數據。對於短的(特別是小於
BUFFSIZE) 的)回顯字符串和典型的連接,while 循環只會執行一次。但是底層的套接字接口 (以及 TCP/IP) 不對字節流將如何在
recv() 調用之間劃分做任何保證。
void HandleClient(int sock) {
char buffer[BUFFSIZE];
int received = -1;
/* Receive message */
if ((received = recv(sock, buffer, BUFFSIZE, 0)) < 0) {
Die("Failed to receive initial bytes from client");
}
/* Send bytes and check for more incoming data in loop */
while (received > 0) {
/* Send back received data */
if (send(sock, buffer, received, 0) != received) {
Die("Failed to send bytes to client");
}
/* Check for more data */
if ((received = recv(sock, buffer, BUFFSIZE, 0)) < 0) {
Die("Failed to receive additional bytes from client");
}
}
close(sock);
}
傳入處理函數的套接字是已經連接到發出請求的客戶機的套接字。一旦完成所有數據的回顯,就應該關閉這個套接字。父服務器套接字被保留下來,以便產生新的子套接字,就像剛剛被關閉那個套接字一樣。
TCP 回顯服務器(配置服務器套接字)
就像前面所介紹的,創建套接字的目的對服務器和對客戶機稍有不同。服務器創建套接字的語法與客戶機相同,但結構 echoserver
是用服務器自己的信息而不是用它想與之連接的對等方的信息來建立的。您通常需要使用特殊常量 INADDR_ANY ,以支持接收服務器提供的任何
IP 地址上的請求;原則上,在諸如這樣的多重主機服務器中,您可以相應地指定一個特定的 IP 地址。
int main(int argc, char *argv[]) {
int serversock, clientsock;
struct sockaddr_in echoserver, echoclient;
if (argc != 2) {
fprintf(stderr, "USAGE: echoserver <port>/n");
exit(1);
}
/* Create the TCP socket */
if ((serversock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
Die("Failed to create socket");
}
/* Construct the server sockaddr_in structure */
memset(&echoserver, 0, sizeof(echoserver)); /* Clear struct */
echoserver.sin_family = AF_INET; /* Internet/IP */
echoserver.sin_addr.s_addr = htonl(INADDR_ANY); /* Incoming addr */
echoserver.sin_port = htons(atoi(argv[1])); /* server port */
注意,無論是IP地址還是端口,它們都要被轉換爲用於 sockaddr_in 結構的網絡字節順序。轉換回本機字節順序的逆向函數是 ntohs() 和 ntohl()。這些函數在某些平臺上不可用,但是爲跨平臺兼容性而使用它們是明智的。
TCP 回顯服務器(綁定和監聽)
雖然客戶機應用程序 connect() 到某個服務器的 IP 地址和端口,但是服務器卻 bind() 到它自己的地址和端口。
/* Bind the server socket */
if (bind(serversock, (struct sockaddr *) &echoserver,
sizeof(echoserver)) < 0) {
Die("Failed to bind the server socket");
}
/* Listen on the server socket */
if (listen(serversock, MAXPENDING) < 0) {
Die("Failed to listen on server socket");
}
一旦幫定了服務器套接字,它就準備好可以 listen() 了。與大多數套接字函數一樣,如果出現問題,bind() 和 listen() 函數都返回 -1。一旦服務器套接字開始監聽,它就準備 accept() 客戶機連接,充當每個連接上的套接字的工廠。
TCP 回顯服務器(套接字工廠)
爲客戶機連接創建新的套接字是服務器的一個難題。函數 accept() 做兩件重要的事情:返回新的套接字的套接字指針;填充指向echoclient(在我們的例子中) 的 sockaddr_in 結構。
/* Run until cancelled */
while (1) {
unsigned int clientlen = sizeof(echoclient);
/* Wait for client connection */
if ((clientsock =
accept(serversock, (struct sockaddr *) &echoclient,
&clientlen)) < 0) {
Die("Failed to accept client connection");
}
fprintf(stdout, "Client connected: %s/n",
inet_ntoa(echoclient.sin_addr));
HandleClient(clientsock);
}
}
我們可以看到 echoclient 中已填充的結構,它調用訪問客戶機 IP 地址的 fprintf()。客戶機套接字指針被傳遞給 HandleClient(),我們在本節的開頭看到了這點。
結束語
在本教程中介紹的服務器和客戶機很簡單,但是它們展示了編寫 TCP
套接字應用程序的每個基本要素。如果所傳輸的數據更復雜,或者應用程序中的對等方(客戶機和服務器)之間的交互更高深,那就是另外的應用程序編程問題了。
即使這樣,所交換的數據仍然遵循 connect() 和 bind() 然後再 send() 和 recv() 的模式。
本教程沒有談及的一件事情是 UDP 套接字的使用,雖然我們在本教程開頭的摘要中提到了。比起 UDP,TCP 使用得更普遍,不過同時理解 UDP 套接字以作爲你編寫應用程序的選擇也是很重要的。