Try   HackMD

Socket 從頭學

本篇內容是我看Sockets Tutorial時所寫下的筆記,可以直接下載他的server.c, client.c執行看結果

gcc -o server server.c
gcc -o client client.c
./server 12345
# 切到令一個terminal
./client 127.0.0.1 12345

Day 1 Hello World, only trace the server side

Knowledge

Hello world使用TCP協定傳輸,下圖為使用TCP協定的流程圖。

Trace code

Server.c

#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> // socket 和 in 會用到 types #include<sys/types.h> // socket在這~ #include<sys/socket.h> // in 中有 internet domain addresses 的結構跟各種定義 #include<netinet/in.h> void error(const char *msg){ // perror會按照以下格式印出error的log // msg: error message corresponding to the current value of // errno and a new-line. /*void perror(const char *s) * s * 在 error message 之前想要印出的東西 * perror會去看當前的errno是多少,從而得知最近一次的error是什麼錯誤 * */ perror(msg); exit(1); } /*Server process * Step 1 create the socket object for listening called server * Step 2 bind * Step 3 listen * Step 4 accept, create a socket for this communication * Step 5 read * Step 6 write * Step 7 close * */ int main(int argc, char *argv[]){ /*int sockfd, newsockfd * 是 socket file descriptor 的意思 * 所以新建的socket物件會被assign給他們 *int portno * 用來放port number,我們需要知道port才能跟對方溝通 *socklen_t client * 存客戶端的地址大小 */ int sockfd, newsockfd, portno; socklen_t client; /*char buffer * 接收資料的地方 * */ char buffer[256]; /*struct sockaddr_in 專用於 IPv4 * sin_family * 要以哪種domain連線,常用有兩種 * AF_UNIX/AF_LOCAL: 用在本機程序與程序間的傳輸,讓兩個程序共享一個檔案系統 * AF_INET/AF_INET6: 讓兩台主機透過網路進行資料傳輸,前者為IPv4,後者為IPv6 * sin_port * 要使用哪個port * struct in_addr sin_addr * 這個struct 中只有一個物件 * unsigned long s_addr * 讓socket 知道你這個server在哪個IP執行 * 常量INADDR_ANY代表的就是你的IP * char sin_zero[8] * 不會用到,應該要是0 * * serv_addr * 有server端的資訊,用struct sockaddr_in表達 * cli_addr * 有client端的資訊,用struct sockaddr_in表達 * * */ struct sockaddr_in serv_addr, cli_addr; /*n * read()和write()的回傳值,裡面會存放傳輸字元的數量 * */ int n; // 如果沒給port,當作失敗無法執行Server if(argc < 2){ fprintf(stderr, "ERROR, no port provided\n"); exit(1); } /* Step 1 create the socket object for listening called server *int socket(int domain, int type, int protocol) * domain * 前面提到的,要在哪個domain通訊 * type * socket的傳輸方法 * SOCK_STREAM: TCP protocol * SOCK_DGRAM: UDP protocol * protocol * 設定socket的協定標準,通常設0,讓kernel選擇type對應的默認協議 * Return Value * 成功:socket file descriptor,可以透過他操作socket * 失敗:-1,並且馬上設定errno * */ sockfd = socket(AF_INET, SOCK_STREAM, 0); // 檢查是否成功建立socket if(sockfd < 0) error("ERROR opening socket"); /*void bzero(void *s, size_t n) * 從地址s開始n個bytes的資料通通覆寫為0 * */ bzero((char *) &serv_addr, sizeof(serv_addr)); // port number 用 portno 接起來 portno = atoi(argv[1]); // 設定 server 的 domain,這裡必須是AF_INET serv_addr.sin_family = AF_INET; // 設定 server 的IP,在 server端 通常是當前機器IP,而 INADDR_ANY是這個IP的常量 serv_addr.sin_addr.s_addr = INADDR_ANY; /* 設定 server 的port *htons * 轉換byte order * 我們平常用的是 host byte order, 通訊協定要的是 network byte order * */ serv_addr.sin_port = htons(portno); /* Step 2 bind *int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen) * sockfd * Step1 建立的 socket file descriptor * addr * 我們設定的資訊,在這裡是server的資訊 * addrlen * server資訊的大小 * Return Value * 成功:0 * 失敗:-1,並且馬上設定errno * bind是用來給socket物件名稱的,建立socket物件時,他只存在於 * name space(address family),但是沒有assigned給任何變數 * 因此要用bind把socket物件assigned給一個名字 * */ if(bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR on binding"); /* Step 3 listen *int listen(int sockfd, int backlog) * sockfd * socket file descriptor * backlog * 可以在線等待server的client數量,當超過這個數量時 * 新的client會收到ECONNREFUSED錯誤 * Return Value * 成功:0 * 失敗:-1,並且馬上設定errno * 這個system call只要有合法的sockfd就會成功,而我們前面已經確定過 * sockfd是否成功建立,因此這裡不需要再做檢查 * */ listen(sockfd, 5); /* Step 4 accept, create a socket for this communication * 程式會停在這一步直到有client請求連線。 *int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) * sockfd * 在等client的 file descriptor * addr * 發送請求端的sockaddr結構,client端的資料會被塞到這裡面 * addrlen * 送來的結構大小 * Return Value * 成功:一個新的 file descriptor,所有通訊會在這個 file descriptor完成 * 失敗:-1,並且馬上設定errno * */ client = sizeof(cli_addr); newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &client); if(newsockfd < 0) error("ERROR on accept"); // 把要接收訊息的容器清乾淨 bzero(buffer, 256); /* Step 5 read * 要注意這裡使用的fd是建立通訊後的fd,而不是再依開始建立的server fd * 所以我可以推測server的socket物件只是用來聽請求的? *ssize_t read(int fd, void *buf, size_t count) * fd * file descriptor,建立起溝通後的file descriptor * buf * 接收資料的容器,會把傳送的資料放到這裡,超過count就會只放count大小的資料 * count * 最大能容許的資料大小 * Return Value * 成功:傳送的bytes數量,0代表EOF * 失敗:-1 * */ n = read(newsockfd, buffer, 255); if(n < 0) error("ERROR reading from socket"); // 把收到的訊息印出來 printf("Here is the message: %s\n", buffer); /* Step 6 write *ssize_t write(int fd, const void *buf, size_t count) * fd * file descriptor,一樣要用溝通的那個file descriptor * buf * 要傳送給client的訊息,大小必須小於count * 否則只傳送count大小的訊息 * count * 傳送的資料量最大值 * Return Value * 成功:傳送的bytes數量 * 失敗:-1,並且馬上設定errno * */ n = write(newsockfd, "I got your message", 18); if(n < 0)error("ERROR writing to socket"); /* Step 7 close *int close(int fd) * fd * 把file descriptor關掉 * Return Value * 成功:0 * 失敗:-1,並且馬上設定errno * */ close(newsockfd); close(sockfd); return 0; }

Q&A


Q: 為什麼 struct sockaddr_in中的sin_family只能是AF_INET?

A: Why does the sin_family member exist?中有回答,使用不同domain會有不同的結構,sockaddr_in是專屬於IPv4的結構,因此只能是AF_INET。而要這樣設定的原因是在bindaccept的中,會使用到sockaddr*來指向當前使用的結構,而他們只需要看這個結構中第一個成員的值就能決定該用哪一種domain,因此仍然需要這個成員。


Q: 為什麼要用bind?

Answer from man 2 bind: bind是用來給socket物件名稱的,建立socket物件時,他只存在於 name space(address family),但是沒有assigned給任何變數,因此要用bind把socket物件assigned給一個名字

我的觀察是在我們一開始建立的socket沒有關於IPport的資訊,我們透過bind把這些資訊給socket。


Q: perror怎麼知道是什麼error?

A: perror會去看當前的errno是多少,從而得知最近一次的error是什麼錯誤


Day 2 Hello World, trace the client side

Trace code

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> // 需要用到裡面的 hostent 結構 #include <netdb.h> void error(const char *msg){ perror(msg); exit(0); } /*Client process * Step 1 create the socket object for connecting to the server called client * Step 2 connect * Step 3 write * Step 4 read * Step 5 close * */ int main(int argc, char* argv[]){ int sockfd, portno, n; // serv_addr 存要連接的server的資訊 struct sockaddr_in serv_addr; /*struct hostent * char *h_name * official name of host * char **h_aliases * alias list * 主機的備用名稱 * int h_addrtype * host address type * 主機會回傳的address type,現在固定是AF_INET * int h_length * length of address, int bytes * char **h_addr_list * list of address from name server * 指向主機的網路地址列表,address 是 newtork byte order * #define h_addr h_addr_list[0] * address, for backward compatiblity * 這是h_addr_list第一個地址的別名 * */ struct hostent *server; char buffer[256]; if(argc < 3){ fprintf(stderr, "usage %s hostname port\n", argv[0]); exit(0); } portno = atoi(argv[2]); // Step 1 create the socket object for connecting tothe server called client // 跟server一樣,都要先建立一個socket物件 sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd < 0) error("ERROR opening socket"); /*struct hostent *gethostbyname(const char *name) * name * hostname or IPv4 address. * 如果是IPv4地址,就可以直接將地址copy到hostnet的h_name和struct in_addr * 如果是hostname而且環境參數HOSTALIASES有備設定,會先去HOSTALIASES找hostname * Return Value * 成功:A pointer of the struct hostent * 失敗:NULL, h_error會馬上被設定成失敗的原因編號 * 根據manual寫的,gethostbyname, gethostbyaddr, herror, hstrerror是過時的方法 * 應該改用 getaddrinfo, getnameinfo, gai_strerror * */ server = gethostbyname(argv[1]); if(server == NULL){ fprintf(stderr, "ERROR, no such host\n"); exit(0); } bzero((char *) &serv_addr, sizeof(serv_addr)); // 跟server端一樣,把各項資訊設定給serv_addr serv_addr.sin_family = AF_INET; /* 因為h->addr是char*,所以我們用bcopy地址複製出來 *void bcopy(const void *src, void *dest, size_t n) * src * The source you wanna copy from. * dest * The destination you wanna copy for. * n * Copy n bytes * */ bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length); serv_addr.sin_port = htons(portno); /* Step 2 connect * 使用connect發送請求,要等server accept才會繼續往下做 *int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) * sockfd * socket file descriptor, 一開始建立起來的socket物件 * addr * address, the information we got from the gethostbyname and then * copy to the serv_addr * addrlen * the size of the addr * Return Value * 成功:0 * 失敗:-1,並且馬上設定errno * */ if(connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) error("ERROR connecting"); printf("Please enter the message: "); bzero(buffer, 256); fgets(buffer, 255, stdin); // Step 3 write n = write(sockfd, buffer, strlen(buffer)); if(n < 0) error("ERROR writing to socket"); bzero(buffer, 256); // Step 4 read n = read(sockfd, buffer, 255); if(n < 0) error("ERROR reading from socket"); printf("%s\n", buffer); // Step 5 close close(sockfd); return 0; }

Q&A


Q: client 建立的socket物件有辦法先知道 server端 使用的 domaintype 再建立嘛?

A: domain可以在gethostbyname後得知,但是type沒辦法


Q: 這樣的server只能有一個連線,然後讀一次訊息就結束了,我希望能更多連線怎麼做?

A: 看Sockets Tutorial的後面,有對server做改進。