1. 需求分析
實現一個回聲伺服器的C/S(客戶端client/伺服器server)程式,功能為客戶端連線到伺服器後,傳送一串字串,伺服器接受資訊後,返回對應字串的大寫形式給客戶端顯示。
例如:
客戶端傳送“this is a webserver example!
",
伺服器返回"THIS IS A WEBSERVER EXAMPLE!
"
2. 專案實現
2.1 伺服器端程式echo_server.c
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <unistd.h> //read, write, close
#include <sys/types.h> //socket, bind, listen, accept
#include <sys/socket.h> //socket, bind, listen, accept
#include <string.h> //strerror
#include <ctype.h> //inet_ntop
#include <arpa/inet.h> //inet_ntop
#include <errno.h> //strerror
#define SERVER_PORT 666
//出錯處理
void perror_exit(const char* des) {
fprintf(stderr, "%s error, reason: %s\n", des, strerror(errno));
exit(1);
}
int main(void){
int sock;//代表信箱
int ret;//作為bind和listen的返回值,用於處理出錯資訊
struct sockaddr_in server_addr;
//1.建立套嵌字(信箱)。成功:返回socket的檔案描述符,失敗:返回-1,設定errno
sock = socket(AF_INET, SOCK_STREAM, 0);
if(sock == -1) {
perror_exit("create socket");
}
//2.清空伺服器地址空間(標籤),寫上地址和埠號
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;//選擇協議族IPV4
//inet_pton(AF_INET, "1.1.1.1", &server_addr.sin_addr.s_addr);//測試出錯處理函式perror_exit
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//監聽本地所有IP地址
server_addr.sin_port = htons(SERVER_PORT);//繫結埠號
//3. 實現標籤貼到收信得信箱上
ret = bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
if(ret == -1) {
perror_exit("bind");
}
//4. 把信箱掛置到傳達室,這樣,就可以接收信件了(監聽客戶端)
ret = listen(sock, 128);
if(ret == -1) {
perror_exit("listen");
}
//萬事俱備,只等來信
printf("等待客戶端的連線\n");
//5. 處理客戶端請求
int done =1;
while(done){
struct sockaddr_in client;
int client_sock, len, i;
char client_ip[64];
char buf[256];
socklen_t client_addr_len;
client_addr_len = sizeof(client);
client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);
//列印客服端IP地址和埠號
printf("client ip: %s\t port : %d\n",
inet_ntop(AF_INET, &client.sin_addr.s_addr,client_ip,sizeof(client_ip)),
ntohs(client.sin_port));
/*讀取客戶端傳送的資料*/
len = read(client_sock, buf, sizeof(buf)-1);
buf[len] = '\0';
printf("receive[%d]: %s\n", len, buf);
//轉換成大寫
for(i=0; i<len; i++){
buf[i] = toupper(buf[i]);
}
len = write(client_sock, buf, len);
printf("finished. len: %d\n", len);
close(client_sock);
}
//6. 關閉連線
close(sock);
return 0;
}
2.2 客戶端程式echo_client.c
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <string.h> //memset, strlen
#include <unistd.h> //read, write, close
#include <sys/socket.h> //socket, connect
#include <arpa/inet.h> //inet_pton
#define SERVER_PORT 666
#define SERVER_IP "127.0.0.1"
int main(int argc, char *argv[]){//argc表示傳入命令的個數,argv表示傳入的具體資訊
int sockfd;
char *message;
struct sockaddr_in servaddr;
int n;
char buf[64];
//異常處理
if(argc != 2){
fputs("Usage: ./echo_client message \n", stderr);
exit(1);
}
message = argv[1];//傳入的資訊
printf("message: %s\n", message);
//1. 建立套嵌字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, '\0', sizeof(struct sockaddr_in));//分配空間
//定義地址IP和埠
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
servaddr.sin_port = htons(SERVER_PORT);
//2. 連線伺服器
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//3. 讀寫和伺服器的互動資訊
write(sockfd, message, strlen(message));
n = read(sockfd, buf, sizeof(buf)-1);
if(n>0){
buf[n]='\0';
printf("receive: %s\n", buf);
}else {
perror("error!!!");
}
printf("finished.\n");
//4. 關閉連線
close(sockfd);
return 0;
}
3. 程式執行方式
我的echo_server.c程式在/share/echo_server資料夾下,echo_client.c程式在/share/echo_client資料夾下。
必須先執行伺服器程式,再執行客戶端程式。順序不能反!!
3.1. 執行伺服器程式
- 首先,進入echo_server.c所在資料夾
root@lxb-virtual-machine:/# cd /share/echo_server
- 之後,編譯程式
root@lxb-virtual-machine:/share/echo_server# gcc echo_server.c -o echo_server
- 最後執行程式
root@lxb-virtual-machine:/share/echo_server# ./echo_server
全過程截圖:
3.2. 執行客戶端程式
- 首先,進入echo_client.c所在資料夾
root@lxb-virtual-machine:/# cd /share/echo_client
- 之後,編譯程式
root@lxb-virtual-machine:/share/echo_client# gcc echo_client.c -o echo_client
- 最後執行程式
root@lxb-virtual-machine:/share/echo_client# ./echo_client "this is a webserver example!"
全過程截圖:
4. 分析
首先我們進行感性的分析,用來理解各個步驟的用意。之後我們需要對裡面涉及到的函式進行具體的分析。
4.1 李華寫信模型
我們在高中英語經常遇到的一道作文題就是“你是李華,請給國外的筆友Andy寫信”,而我們網路通訊也可以類比於“李華與國外筆友通訊”的模型。這裡我們將“李華”比作客戶端,“國外筆友Andy”作為伺服器端。
4.1.1 Andy應該怎麼做呢?
為了使“李華同學”與“國外筆友”能夠交流,首先需要統一語言,同時約定好寄信方式,郵局寄信還是電子郵件之類的,(這就是“socket套嵌字”)。之後Andy準備好一個信箱,之後找一張標籤紙(server_addr),整理乾淨這張標籤紙(bzero函式),往上面寫上自己的地址和門牌號,一切準備好後,將貼好標籤紙的信箱掛到外面(listen函式),這樣大家都能給Andy寄信。最後Andy只需要時不時去看看信箱有沒有信,有的話把信的內容讀出來(read函式),之後再寫封回信寄回去(write函式)。
4.1.2 李華應該怎麼做?
李信作為寫信人就比較簡單了,首先還是使用統一的寄信方式,往信封上寫上自己要寄的地址和門牌號,也就是Andy家的地址和門牌號,之後與Andy聯絡上(connect函式)。接下來就可以給Andy寫信(write函式),讀Andy的回信(read函式)。收到回信,不想再和Andy通訊了,這時就把兩個人的聯絡斷開(close函式)。
4.2 程式流程圖
4.3 具體函式解析
4.3.1 socket函式
-
所屬標頭檔案
#include <sys/socket.h>
-
函式定義
int socket(int domain, int type, int protocol);
-
引數含義
- domain:
AF_INET 這是大多數用來產生socket的協議,使用TCP或UDP來傳輸,用IPv4的地址
AF_INET6 與上面類似,不過是來用IPv6的地址
AF_UNIX 本地協議,使用在Unix和Linux系統上,一般都是當客戶端和伺服器在同一臺及其上的時候使用 - type:
SOCK_STREAM 這個協議是按照順序的、可靠的、資料完整的基於位元組流的連線。這是一個使用最多的socket型別,這個socket是使用TCP來進行傳輸。
SOCK_DGRAM 這個協議是無連線的、固定長度的傳輸呼叫。該協議是不可靠的,使用UDP來進行它的連線。
SOCK_SEQPACKET該協議是雙線路的、可靠的連線,傳送固定長度的資料包進行傳輸。必須把這個包完整的接受才能進行讀取。
SOCK_RAW socket型別提供單一的網路訪問,這個socket型別使用ICMP公共協議。(ping、traceroute使用該協議)
SOCK_RDM 這個型別是很少使用的,在大部分的作業系統上沒有實現,它是提供給資料鏈路層使用,不保證資料包的順序 - protocol:
傳0 表示使用預設協議。 - 返回值:
成功:返回指向新建立的socket的檔案描述符,失敗:返回-1,設定errno
- domain:
4.3.2 bind函式
-
所屬標頭檔案
#include <sys/socket.h>
-
函式定義
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
引數含義
- sockfd:
socket檔案描述符 - addr:
IP地址加埠號 - addrlen:
addr的長度 - 返回值:
成功:返回0,失敗:返回-1,設定errno
- sockfd:
4.3.3 listen函式
-
所屬標頭檔案
#include <sys/socket.h>
-
函式定義
int listen(int sockfd, int backlog);
-
引數含義
- sockfd:
socket檔案描述符 - backlog:
在Linux 系統中,它是指排隊等待建立3次握手佇列長度 - 返回值:
成功:返回0,失敗:返回-1,設定errno
- sockfd:
4.3.4 accept函式
-
所屬標頭檔案
#include <sys/socket.h>
-
函式定義
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
引數含義
- sockfd:
socket檔案描述符 - addr:
IP地址加埠號 - addrlen:
addr的長度 - 返回值:
成功:返回一個新的socket檔案描述符,失敗:返回-1,設定errno
- sockfd:
4.3.5 connect函式
-
所屬標頭檔案
#include <sys/socket.h>
-
函式定義
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
引數含義
- sockfd:
socket檔案描述符 - addr:
IP地址加埠號 - addrlen:
addr的長度 - 返回值:
成功:返回一個新的socket檔案描述符,失敗:返回-1,設定errno
- sockfd:
4.3.6 出錯處理函式
-
所屬標頭檔案
#include <errno.h> #include <string.h>
-
函式定義
char *strerror(int errnum);
-
引數含義
- errnum:
錯誤編號的值,一般取 errno 的值 - 返回值:
錯誤原因
- errnum:
5.感謝
感謝bilibili的Martin老師的視訊: C語言/C++伺服器開發】小白實現第一個伺服器入門專案 網路通訊與Socket 程式設計詳解&原始碼分享,本篇部落格也是基於Martin老師這個視訊所做的。