在類 Unix 的操作系統中,I/O 操作都是通過讀寫文件描述符(file descriptor)來進行的。文件描述符是類 Unix 系統中的一個重要概念。socket 操作的是網絡 I/O,所以也沿用這種設計思路。socket 有對應的套接字描述符 sockfd。很多用于操作文件描述符 fd 的操作都可以用于操作 sockfd。就如常用的文件操作 write()/read() 就可以用于 sockfd,來實現數據的發送和接收。
在接著介紹 socket 之前,先跳出來了解一下文件描述符
文件描述符(File descriptor,后文用 fd 替代),是一個非負整數。當進程打開或者創建一個文件時,系統內核就會向進程返回一個 fd 用來指代這個文件。對文件讀寫時,就將 fd 作為參數傳遞給 write() / read() 函數。
在 Unix shell 中,文件描述符0與進程標準輸入相關聯,文件描述符1與標準輸出相關聯,文件描述符2與標準錯誤相關聯。
下面這張圖描述了,進程、文件描述符以及文件間的關系:
繼續介紹 socket
socket 地址
/*==================================
* socket 的地址結構,作為 bind(),
* connect(),accept() 的參數
*================================*/
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
/*==================================
* sockaddr_in 是 IPv4 因特網地址,
* 是具體的 sockaddr。使用時需要強制
* 轉換成 sockaddr
* sin_family = AF_INET
*================================*/
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
/*==================================
* 4字節的 IPv4 地址
*================================*/
struct in_addr {
unsigned long s_addr;
};
這些地址是用于 socket 函數的。sockaddr_in 是專用于 IPv4 通信的。IPv6 的地址結構,這里未列出
socket 的地址分為 4類:
- AF_INET, IPv4 因特網域, 用于不同 pc 通過網絡來通信
- AF_INET6, IPv6 因特網域
- AF_UNIX, (AF_LOCAL)UNIX 域, 用于同一臺 pc 下不同進程間通信
- AF_UNSPEC, 未指定
可以看出 socket 不僅可以使用 ip 地址作為 sockaddr,還能使用其他協議族的地址。這正和 socket 的設計目標一致:同樣的接口既可以用于計算機間通信還能用于計算機內通信
socket 函數
#include <sys/socket.h>
//建立連接
int socket(int domain, int type, int protocal);
int bind(int sockfd, const struct sockaddr *addr, socklent_t len);
int connect(int sockfd, const struct sockaddr *addr, socklen_t lent);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);
//數據傳輸
ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t nbytes, int flags, const struct sockaddr *destaddr, socklen_t destlen);
ssize_t sendmsg(int socklen, const struct msghdr *msg, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//關閉連接
#include <unistd.h>
int close(int sockfd);
#include <sys/socket.h>
int shutdown(int sockfd, int how);
note:
1、strict 關鍵字, 用于告知編譯器, strict 修飾的指針所指向的內容,只能通過這個指針修改。沒有其他修改途徑,有利于后面的代碼優化
上面將 socket 相關函數分為了三類: 建立連接、傳輸數據、關閉連接。這么分的目的是為了和 TCP 傳輸的過程相對應。盡管 socket 并不僅僅用于 tcp 傳輸,這里為了和我們熟知的網絡知識關聯起來,就以 tcp 連接為例來介紹 socket
對于一個 c/s 模型的服務來說,client 和 server 間的通信可以簡化以下兩個部分:
client
- sockfd = socket( AF_INET, SOCK_STREAM, 0 )
- connect( sockfd, &serv_addr, sizeof(serv_addr) )
- wirte( sockfd, buffer, strlen(buffer) );
客戶端創建一個 socket ,返回一個套接字描述符 sockfd。接著,connect() 函數,向服務器發起連接。這個步驟可以看成,client 和 server 進行 tcp 三次握手。
server
- sockfd = socket( AF_INET, SOCK_STREAM, 0 )
- bind(sockfd, &serv_addr, sizeof(serv_addr) )
- listen(sockfd, 1024);
- newsockfd = accept(sockfd, &cli_addr, sizeof(cli_addr) );
服務端創建一個 socket,返回一個套接字描述符 sockfd。bind( ) 將服務器的地址和其中一個端口綁定到 sockfd 上。調用 listen( ) ,開始在綁定的端口上監聽來自客戶端的連接。當有新的客戶端連接到來時,調用 accept( ) 創建一個新的套接字描述符 newsockfd , 來處理這個新連接。之前的 sockfd 繼續監聽是否有新連接。
這個過程,可以與下面這張圖對應起來
在前文中,介紹了最為典型的 socket 類型 Stream Sockets。stream socket 主要用于 tcp 傳輸
除此之外,socket 還有其他 3種類型
- Data gram Sockets ,用于 udp 傳輸
- Raw Sockets ,用來訪問底層協議,主要用來開發新的協議
- Sequenced Packet Sockets,和 stream sockets 相似,但是它保留了邊界