本篇內容是我看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
Hello world使用TCP協定傳輸,下圖為使用TCP協定的流程圖。
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: 為什麼 struct sockaddr_in
中的sin_family
只能是AF_INET
?
A: Why does the sin_family member exist?中有回答,使用不同domain會有不同的結構,sockaddr_in
是專屬於IPv4的結構,因此只能是AF_INET
。而要這樣設定的原因是在bind
和accept
的中,會使用到sockaddr*
來指向當前使用的結構,而他們只需要看這個結構中第一個成員的值就能決定該用哪一種domain,因此仍然需要這個成員。
Q: 為什麼要用bind?
Answer from man 2 bind: bind是用來給socket物件名稱的,建立socket物件時,他只存在於 name space(address family),但是沒有assigned給任何變數,因此要用bind把socket物件assigned給一個名字
我的觀察是在我們一開始建立的socket沒有關於IP
或port
的資訊,我們透過bind
把這些資訊給socket。
Q: perror怎麼知道是什麼error?
A: perror會去看當前的errno是多少,從而得知最近一次的error是什麼錯誤
#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: client 建立的socket
物件有辦法先知道 server端 使用的 domain
跟 type
再建立嘛?
A: domain
可以在gethostbyname
後得知,但是type
沒辦法
Q: 這樣的server只能有一個連線,然後讀一次訊息就結束了,我希望能更多連線怎麼做?
A: 看Sockets Tutorial的後面,有對server做改進。
感謝朋友 R 的幫忙,在 mock interview 時常常會崩潰,但他都會情緒穩定的開導我。也感謝家人跟女朋友的鼓勵及支持。
Mar 23, 2025第1章討論了 Linux 核心網絡子系統的角色及其運作的三個層次。Netlink socket 介面首次出現在 2.2 版的 Linux 核心中,作為 AF_NETLINK socket。它被創建為比笨拙的 IOCTL 通訊方法更靈活的替代方案。IOCTL 處理程序無法從核心向用戶空間發送非同步消息,而 netlink sockets 則可以。使用 IOCTL 還有另一層複雜性:需要定義 IOCTL 編號。Netlink 的操作模型相當簡單:使用者在 userspace 使用 socket API 打開並註冊一個 netlink socket,這個 netlink socket 處理與核心 netlink socket 的雙向通信,通常用於發送消息來配置各種系統設置並從核心獲取回應。本章描述了 netlink 協議的實現和 API,並討論了其優缺點。本書還介紹了新的通用 netlink 協議,討論了其實現和優點,並給出了一些使用 libnl 庫的示例。最後,討論了 socket 監控介面。
Jun 8, 2024本章將重點介紹位於 ISO 模型第7層的 ICMP 協定,並詳細解析該協定的數據包在系統內核中的處理過程。在用戶空間層面,使用者能夠通過 socket API 來傳送 ICMP 數據包,其中,ping 命令是一個廣為人知且常用的例子。
Jun 5, 2024source:
May 27, 2024or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up