介紹
對比簡易的 TCP 伺服器與客戶端,本文中的伺服器與客戶端程式改進了以下幾點:
使用
inet_ntop()函數,讓伺服器程式能正確處理 IPv4 與 IPv6 位址。使用
getaddrinfo()函數,讓伺服器程式能正確處理 IPv4 與 IPv6 位址。使用
fork()函數,讓伺服器程式能同時處理多個客戶端的連線請求。使用
waitpid()函數,讓伺服器程式能正確處理子行程的結束。
伺服端程式碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT "43866" /* 連接埠號 */
#define BACKLOG 10 /* 最大連線請求佇列長度 */
#define MAXDATASIZE 100 /* 最大資料傳輸量 */
/* 訊號處理函式,用於處理子行程結束 */
void sigchld_handler(int s)
{
/* 因為 waitpid() 可能會改變 errno 的值,所以先儲存,然後還原 */
int saved_errno = errno;
/* 等待所有子行程結束 */
while (waitpid(-1, NULL, WNOHANG) > 0)
;
/* 還原 errno */
errno = saved_errno;
}
/* 取得 sockaddr 結構體的 IP 位址 */
void *get_in_addr(struct sockaddr *sa)
{
/* 判斷位址家族 */
if (sa->sa_family == AF_INET)
{
/* 回傳 IPv4 位址 */
return &(((struct sockaddr_in *)sa)->sin_addr);
}
/* 回傳 IPv6 位址 */
return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}
/* 取得 sockaddr 結構體的連接埠號 */
int get_in_port(struct sockaddr *sa)
{
/* 判斷位址家族 */
if (sa->sa_family == AF_INET)
{
/* 回傳 IPv4 連接埠號 */
return (((struct sockaddr_in *)sa)->sin_port);
}
/* 回傳 IPv6 連接埠號 */
return (((struct sockaddr_in6 *)sa)->sin6_port);
}
/* 主函式 */
int main(int argc, char *argv[])
{
int sockfd, new_fd; /* sockfd:監聽 socket, new_fd:資料傳輸 socket */
struct addrinfo hints, *servinfo, *p; /* hints: 用於設定 addrinfo 結構體, servinfo: 存放位址資訊, p: addrinfo 指標,用於走訪 servinfo */
struct sockaddr_storage their_addr; /* 用於存放用戶端 IP 位址 與 連接埠號 資訊 */
socklen_t sin_size; /* 用於 accept() */
struct sigaction sa; /* 用於 sigaction() */
int yes = 1; /* 用於設定 setsockopt() 函式 */
int rv; /* 用於存放 getaddrinfo() 函式的回傳值 */
char s[INET6_ADDRSTRLEN]; /* 用於存放 inet_ntop() 函式回傳的位址資訊字串 */
int numbytes_send; /* 用於存放 send() 函式的回傳值 */
int numbytes_recv; /* 用於存放 recv() 函式的回傳值 */
char buf[MAXDATASIZE]; /* 用於存放接收到的資料 */
/* 初始化 hints 結構體 */
memset(&hints, 0, sizeof hints);
/* 設定 hints 結構體 */
hints.ai_family = AF_UNSPEC; /* 不指定 IPv4 或 IPv6 */
hints.ai_socktype = SOCK_STREAM; /* TCP stream sockets */
hints.ai_flags = AI_PASSIVE; /* 使用本機 IP */
/* 取得位址資訊 */
if ((rv = getaddrinfo(NULL, PORT, &hints, &servinfo)) != 0)
{
/* 印出錯誤訊息 */
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
/* 走訪 servinfo 結構體鏈結串列 */
for (p = servinfo; p != NULL; p = p->ai_next)
{
/* 建立 socket */
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
/* 印出錯誤訊息 */
perror("server: socket");
continue;
}
/* 設定 socket 選項 */
// SO_REUSEADDR 選項用於允許重複使用本地位址與連接埠
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1)
{
/* 印出錯誤訊息 */
perror("setsockopt");
exit(1);
}
/* 綁定 socket */
if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
/* 連線伺服器失敗 */
close(sockfd);
perror("server: bind");
continue;
}
break;
}
/* 釋放結果鏈結串列 */
freeaddrinfo(servinfo);
/* 檢查是否成功綁定 */
if (p == NULL)
{
fprintf(stderr, "server: failed to bind\n");
exit(1);
}
/* 監聽 socket */
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen");
exit(1);
}
/* 訊號處理函式 */
sa.sa_handler = sigchld_handler; /* 處理所有殭屍行程 */
sigemptyset(&sa.sa_mask); /* 屏蔽所有訊號 */
sa.sa_flags = SA_RESTART; /* 重啟被中斷的系統呼叫 */
if (sigaction(SIGCHLD, &sa, NULL) == -1)
{
/* 印出錯誤訊息 */
perror("sigaction");
exit(1);
}
printf("server: waiting for connections...\n");
/* 主迴圈 for accept() */
while (1)
{
sin_size = sizeof their_addr;
/* 接受連線請求 */
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
if (new_fd == -1)
{ /* 接受連線請求失敗 */
perror("accept");
continue;
}
/* 將 IP 位址轉換為字串 */
inet_ntop(their_addr.ss_family, get_in_addr((struct sockaddr *)&their_addr), s, sizeof s);
printf("server: got connection from %s, port %d, socketfd %d\n", s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* 建立子行程來處理新連線請求 */
if (!fork())
{
/* 子行程不需要監聽 socket */
close(sockfd);
/* 子行程處理資料傳輸 */
/* 建構傳遞訊息 */
char *msg = "Hello world from server!";
int msg_len = strlen(msg);
/* 傳送訊息到用戶端 */
if ((numbytes_send = send(new_fd, msg, msg_len, 0)) == -1)
perror("send");
/* 印出傳送訊息 */
printf("server: sent message: %s (%d bytes) to %s, port %d, sockfd %d\n", msg, numbytes_send, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* 接收用戶端訊息 */
if ((numbytes_recv = recv(new_fd, buf, MAXDATASIZE - 1, 0)) == -1)
{
perror("recv");
exit(1);
}
buf[numbytes_recv] = '\0';
printf("server: received message: %s (%d bytes) from %s, port %d, sockfd %d\n", buf, numbytes_recv, s, ntohs(get_in_port((struct sockaddr *)&their_addr)), new_fd);
/* 關閉資料傳輸 socket */
close(new_fd);
printf("\n");
/* 結束子行程 */
exit(0);
}
/* 父行程不需要資料傳輸 socket */
close(new_fd);
}
return 0;
}
/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */
用戶端程式碼
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT "43866" /* 連接埠號 */
#define MAXDATASIZE 100 /* 每次最大資料傳輸量 */
/* 取得 sockaddr,IPv4 或 IPv6: */
void *get_in_addr(struct sockaddr *sa)
{
/* 判斷位址家族 */
if (sa->sa_family == AF_INET)
{
/* 回傳 IPv4 位址 */
return &(((struct sockaddr_in *)sa)->sin_addr);
}
/* 回傳 IPv6 位址 */
return &(((struct sockaddr_in6 *)sa)->sin6_addr);
}
int main(int argc, char *argv[])
{
int sockfd; /* sockfd: 用於存放 socket() 函式的回傳值 */
char buf[MAXDATASIZE]; /* 緩衝區,用於存放接收到的資料 */
struct addrinfo hints, *servinfo, *p; /* hints: 用於設定 addrinfo 結構體, servinfo: 存放位址資訊, p: addrinfo 指標,用於走訪 servinfo */
int rv; /* 用於存放 getaddrinfo() 函式的回傳值 */
char s[INET6_ADDRSTRLEN]; /* 用於存放 inet_ntop() 函式回傳的位址資訊字串 */
int numbytes_recv; /* 用於存放 recv() 函式的回傳值 */
int numbytes_send; /* 用於存放 send() 函式的回傳值 */
/* 檢查參數個數 */
if (argc != 2)
{
fprintf(stderr, "usage: ./client hostname\n");
exit(1);
}
/* 初始化 hints 結構體 */
memset(&hints, 0, sizeof hints);
/* 設定 hints 結構體 */
hints.ai_family = AF_UNSPEC; /* 不指定 IPv4 或 IPv6 */
hints.ai_socktype = SOCK_STREAM; /* TCP stream sockets */
/* 取得位址資訊 */
if ((rv = getaddrinfo(argv[1], PORT, &hints, &servinfo)) != 0)
{
/* 印出錯誤訊息 */
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
return 1;
}
/* 走訪 servinfo 結構體鏈結串列 */
for (p = servinfo; p != NULL; p = p->ai_next)
{
/* 建立 socket */
if ((sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
{
/* 印出錯誤訊息 */
perror("client: socket");
continue;
}
/* 連線伺服器 */
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == -1)
{
/* 連線伺服器失敗 */
close(sockfd);
perror("client: connect");
continue;
}
break;
}
/* 走訪完 servinfo 結構體,仍然沒有連線成功 */
if (p == NULL)
{
fprintf(stderr, "client: failed to connect to server\n"); /* 傳送資料到伺服器 */
return 2;
}
/* 將位址資訊轉換為字串 */
inet_ntop(p->ai_family, get_in_addr((struct sockaddr *)p->ai_addr), s, sizeof s);
/* 印出連線的伺服器的位址資訊 */
printf("client: connecting to %s port %s\n", s, PORT);
/* 釋放 servinfo 結構體 */
freeaddrinfo(servinfo);
/* 接收資料 */
if ((numbytes_recv = recv(sockfd, buf, MAXDATASIZE - 1, 0)) == -1)
{
/* 接收資料失敗 */
perror("recv");
exit(1);
}
/* 加入字串結束符號 */
buf[numbytes_recv] = '\0';
/* 印出接收到的資料 */
printf("client: received '%s' from server.\n", buf);
/* 建構待傳送的資料 */
char *msg = "Hello world from client!";
int msg_len = strlen(msg);
/* 傳送資料到伺服器 */
if ((numbytes_send = send(sockfd, msg, msg_len, 0)) == -1)
{
/* 傳送資料失敗 */
perror("send");
}
printf("client: sent '%s' (%d bytes) to server.\n", msg, numbytes_send);
/* 關閉 socket */
close(sockfd);
printf("client: closed socketfd %d.\n", sockfd);
return 0;
}
/* References: Page 31-33, Beej's Guide to Network Programming Using Internet Sockets */
編譯
gcc server.c -o server
gcc client.c -o client
執行與輸出
$ ./server
server: waiting for connections...
server: got connection from 127.0.0.1, port 56836, socketfd 4
server: sent message: Hello world from server! (24 bytes) to 127.0.0.1, port 56836, sockfd 4
server: received message: Hello world from client! (24 bytes) from 127.0.0.1, port 56836, sockfd 4
$ ./client localhost
client: connecting to
client: received 'Hello world from server!' from server.
client: sent 'Hello world from client!' (24 bytes) to server.
client: closed socketfd 3
如果在伺服端執行前執行用戶端,會印出如下錯誤訊息:
$ ./client localhost
client: connect: Connection refused
client: failed to connect to server