1,什麼是套接字
套接字(socket)是一種通信機制,憑借這種機制,客戶/伺服器系統的開發工作既可以在本地單機上進行,也可以跨網絡進行。例如連接配接資料庫,提供Web頁面,遠端登陸等。
2,套接字的屬性
套接字的特性由3個屬性決定:域(domain),類型(type)和協定(protocol)。
2.1 套接字的域
套接字的域指定通信中使用的網絡媒體。最常見的套接字域是AF_INET,它指的是Internet網絡,其底層的協定是--網際協定(IP)。客戶可以通過IP位址和端口号來指定一台聯網機器上的特定服務。
常見的套接字域:
域 | 說明 |
AF_UNIX | UNIX域協定(文本系統套接字 |
AF_INET | ARPA網際網路協定(UNIX網絡套接字 |
AF_ISO | ISO标準協定 |
AF_NS | 施樂(Xerox)網絡系統協定 |
AF_IPX | Novell IPX協定 |
AF_APPLEEALK | Appletalk DDS |
2.2 套接字類型
一個套接字域可能有多種不同的通訊方式,每種通信方式又有其不同的特性。
網際網路協定提供了兩種通信機制:流(stream)和資料報(datagram)。
流套接字提供的是一個有序,可靠,雙向位元組流的連接配接。流套接字由類型SOCK_STREAM指定,它們是在AF_INET域中通過TCP/IP連接配接實作的。
資料報套接字與流套接字相反,它提供的是一種無序的不可靠服務,由類型SOCK_DGRAM指定,它不建立和維持一個連接配接。資料報作為一個單獨的網絡消息被傳輸,它可能丢失,複制或亂序到達。在AF_INET域中通過UDP/IP連接配接實作。
2.3 套接字協定
隻要底層的傳輸機制允許不止一個協定來提供要求的套接字類型,我們就可以為套接字選擇一個特定的協定。常用的協定有,IPPROTO_TCP、IPPROTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分别對應TCP傳輸協定、UDP傳輸協定、STCP傳輸協定、TIPC傳輸協定。注意并不是套接字類型和協定可以随便組合,當協定(protocol)參數為0時,會根據域(domain)和類型(type)選擇對應的預設協定。
3 簡單的用戶端和伺服器端編寫流程
用戶端:
1,為客戶建立一個套接字(socket)
2,根據伺服器情況配置端口和IP位址(sockaddr_in, sockaddr_un)
3,請求連接配接到伺服器(connect)
4,接收資訊或發送資訊(read, write, send, recv)
5,關閉套接字(close)
伺服器端:
1,為伺服器建立一個套接字(socket)
2,配置端口和IP位址(sockaddr_in, sockaddr_un)
3,命名套接字,關聯到特定的位址和端口(bind)
4,建立套接字隊列,監聽伺服器請求(listen)
5,接受連接配接(accept)
6,接收資訊或發送資訊(read, write, send, recv)
7,關閉套接字(close)

4 具體函數描述
4.1 建立套接字
通過socket函數調用建立一個套接字并傳回一個描述符,該描述符可以用來通路該套接字。
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
建立的套接字是一條通信線路的一個端點,用戶端套接字和伺服器端套接字是一條通信線路的兩個端點。domain參數指定協定族,也就是前面講的域,type參數指定這個套接字的通信類型(流或資料報等),protocol參數指定使用的協定。套接字的屬性就是由這三個參數來指定的。
4.2 套接字位址
每個套接字域都有自己的位址格式。對于AF_UNIX域套接字來說,它的位址由結構sockaddr_un來描述,該結構定義在頭檔案sys/un.h中。
struct sockaddr_un {
sa_family_t sun_family; /*AF_UNIX */
char sun_path[]; /*pathname */
};
在AF_INET域中,套接字位址由結構sockaddr_in來指定,該結構定義在頭檔案netinet/in.h中,它至少包含以下幾個成員:
struct sockaddr_in {
short int sin_family; /* AF_INET */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};
IP位址結構in_addr被定義為:
struct in_addr {
unsigned long int s_addr;
};
IP位址中的4個位元組組成一個32位的值。一個AF_INET套接字由它的域,IP位址和端口号來完全确定。
4.3 命名套接字
要想讓通過socket調用建立的套接字可以被其他程序使用,伺服器程式就必須給該套接字命名。這樣,AF_UNIX套接字就會關聯到一個檔案系統的路徑名。AF_INET套接字就會關聯到一個IP端口号。
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);
Bind系統調用把參數address中的位址配置設定給與檔案描述符socket關聯的未命名套接字。位址結構的長度由參數address_len傳遞。
位址的長度和格式取決于位址族。Bind調用需要将一個特定的位址結構指針轉換為指向通用位址類型(struct sockaddr *)。
Bind調用成功時傳回0,失敗時傳回-1并設定errno值。
4.4 建立套接字隊列
為了能夠在套接字上接受進入的連接配接,伺服器程式必須建立一個隊列來儲存未處理的請求。它用listen系統調用來完成這一工作。
#include <sys/socket.h>
int listen(int socket, int backlog);
參數backlog為隊列可以容納未處理連接配接的最大數目,若等待處理的連接配接個數超過這個值,其後的連接配接将被拒絕。Listen函數在成功時傳回0,失敗時傳回-1。
4.5 接受連接配接
當伺服器程式建立并命名了套接字之後,它就可以通過accept系統調用來等待客戶建立對該套接字的連接配接。
#include <sys/socket>
int accept(int socket, struct sockaddr *address, size_t *address_len);
Accept系統調用隻有當有客戶程式試圖連接配接到由socket參數指定的套接字上時才傳回。Aceept函數将建立一個新的套接字來與該客戶進行通信,并且傳回新套接字的描述符。
參數socket必須事先由bind調用命名,并且由listen調用給它配置設定一個連接配接隊列。連接配接客戶的位址将被放入address參數指定的sockaddr結構中。參數address_len指定客戶結構的長度,如果客戶位址的長度超過這個值,它将被截斷。當這個調用傳回時,address_len将被設定為連接配接客戶位址結構的實際長度。
如果套接字隊列中沒有未處理的連接配接,accept将阻塞(程式将暫停)直到有客戶連接配接為止。當然你也可以使用函數fcntl改變這一行為。
4.6 請求連接配接(用戶端)
客戶程式通過一個未命名的套接字和伺服器監聽套接字之間建立連接配接的方法來連接配接到伺服器。它們通過connect調用來完成這一工作。
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len)
參數socket指定的套接字将連接配接到參數address指定的伺服器套接字,address指向的結構的長度由參數address_len指定。
成功時,connect調用傳回0,失敗時傳回-1。
4.7 接收和發送資訊
我們可以通過read/write,recv/send 等函數來接收和發送資料。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
參數fd是我們通過socket函數調用建立套接字時傳回的描述符,read函數是讀取fd對應的套接字中的資料到buf中,write函數是想套接字中寫入buf中的資料。Count參數是控制資料的大小。
當read函數讀取成功時,将傳回實際讀取的位元組數,如果傳回的值是0表示已經讀到檔案的結束了,小于0表示出現了錯誤。同樣對于write函數寫入成功時,将傳回寫入的位元組數,失敗時傳回-1。
4.8 關閉套接字
通過調用close函數來終止伺服器和客戶上的套接字。你應該總是在連接配接的兩端都關閉套接字。
5 簡單執行個體
用戶端: client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ch = 'A';
//為客戶建立一個套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//定義sockaddr_in,配置套接字位址,與伺服器保持一緻,
address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = htons(9734);
len = sizeof(address);
//請求連接配接伺服器
result = connect(sockfd, (struct sockaddr *)&address, len);
//若連接配接失敗,列印失敗資訊,并退出程式
if(result == -1) {
perror("oops: client1");
exit(0);
}
//向客戶套接字中寫入資料,内容是 'A',位元組數為1
write(sockfd, &ch, 1);
//讀取客戶套接字中的一個位元組的資料
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
//關閉套接字
close(sockfd);
exit(0);
}
伺服器端: server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;
//為伺服器建立一個未命名的套接子:
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
//配置套接字位址
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
server_address.sin_port = htons(9734);
server_len = sizeof(server_address);
//命名套接字,關聯到特定的位址和IP端口
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);
//建立一個連結隊列,開始等待客戶進行連接配接:
listen(server_sockfd, 5);
while(1) {
char ch;
printf("server waitting\n");
//接受一個連接配接
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
//對client_sockfd套接子上的客戶進行讀寫操作
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
//關閉套接字
close(client_sockfd);
}
}
在終端上運作指令:
gcc client.c -o client
gcc server.c -o server
./server
./client
輸出:char from server = B
在上面的例子中的inet_addr("127.0.0.1")語句,意思是用inet_addr函數将IP位址的文本表示方式轉換為符合套接字位址要求的格式。還有一個地方需要注意的是語句htons(9734),這是把主機位元組序轉換成了網絡位元組序。相關的内容下節将會說明。
6 主機位元組序和網絡位元組序
在不同的計算機中,會使用不同的位元組序來表示整數。例如,intel處理器将32位的整數分為4個連續的位元組,并以位元組序1-2-3-4存儲到記憶體中,這裡的1表示高位位元組,在記憶體中存儲在起始位址,這種位元組序稱為大端(big-endian)位元組序。而IBM PowerPC處理器是以位元組序4-3-2-1的方式存儲,低位位元組4存儲在起始位址,這種位元組序稱為小端(little-endian)位元組序。
我們把某個給定系統所用的位元組序稱為主機位元組序(host byte order)。為了使不同的計算機可以就通過網絡傳輸的多位元組整數的值達成一緻,你需要定義一個網絡位元組序(network byte order)。網絡位元組序使用大端位元組序來傳送這些多位元組整數。客戶和伺服器程式必須在傳輸之前,将它們的内部整數表示方式轉換為網絡位元組序。它們通過在頭檔案netinet/in.h中的函數來完成這一工作。
#include <netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
這些函數将16位和32位整數在主機位元組序和标準的網絡位元組序之間進行轉換。函數名是與之對應的的轉換操作的簡寫形式。例如“host to network,long”(htonl, 長整數從主機位元組序到網絡位元組序的轉換)和”host to network, short”(htons, 短整數從主機位元組序到網絡位元組序的轉換)。
參考:
1, Linux程式設計
2,UNIX網絡程式設計卷一