socket編程實現tcp伺服器_C/C++
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老師這個影片所做的。