---
tags: 自學
---
# Socket 從頭學
本篇內容是我看[Sockets Tutorial](https://www.linuxhowtos.org/C_C++/socket.htm)時所寫下的筆記,可以直接下載他的[server.c](https://www.linuxhowtos.org/data/6/server.c), [client.c](https://www.linuxhowtos.org/data/6/client.c)執行看結果
```shell!
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
```clike!=
#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?](https://stackoverflow.com/questions/57779761/why-does-the-sin-family-member-exist)中有回答,使用不同domain會有不同的結構,`sockaddr_in`是專屬於IPv4的結構,因此只能是`AF_INET`。而要這樣設定的原因是在`bind`和`accept`的中,會使用到`sockaddr*`來指向當前使用的結構,而他們只需要看這個結構中第一個成員的值就能決定該用哪一種domain,因此仍然需要這個成員。
---
Q: 為什麼要用bind?
Answer from [man 2 bind](https://man7.org/linux/man-pages/man2/bind.2.html): bind是用來給socket物件名稱的,建立socket物件時,他只存在於 name space(address family),但是沒有assigned給任何變數,因此要用bind把socket物件assigned給一個名字
我的觀察是在我們一開始建立的socket沒有關於`IP`或`port`的資訊,我們透過`bind`把這些資訊給socket。
---
Q: perror怎麼知道是什麼error?
A: perror會去看當前的errno是多少,從而得知最近一次的error是什麼錯誤
---
## Day 2 Hello World, trace the client side
### Trace code
```clike!=
#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端 使用的 `domain` 跟 `type` 再建立嘛?
A: `domain`可以在`gethostbyname`後得知,但是`type`沒辦法
---
Q: 這樣的server只能有一個連線,然後讀一次訊息就結束了,我希望能更多連線怎麼做?
A: 看[Sockets Tutorial](https://www.linuxhowtos.org/C_C++/socket.htm)的後面,有對server做改進。