socket API原本是爲網絡通訊設計的,但後來在socket的框架上發展出一種IPC機制,就是UNIXDomain Socket。雖然網絡socket也可用於同一臺主機的進程間通訊(通過loopback地址127.0.0.1),但是UNIX Domain Socket用於IPC更有效率:不需要經過網絡協議棧,不需要打包拆包、計算校驗和、維護序號和應答等,只是將應用層數據從一個進程拷貝到另一個進程。這是因爲,IPC機制本質上是可靠的通訊,而網絡協議是爲不可靠的通訊設計的。UNIX Domain Socket也提供面向流和麪向數據包兩種API接口,類似於TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不會丟失也不會順序錯亂。
UNIX Domain Socket是全雙工的,API接口語義豐富,相比其它IPC機制有明顯的優越性,目前已成爲使用最廣泛的IPC機制,比如X Window服務器和GUI程序之間就是通過UNIX Domain Socket通訊的。
使用UNIX Domain Socket的過程和網絡socket十分相似,也要先調用socket()創建一個socket文件描述符,address family指定爲AF_UNIX,type可以選擇SOCK_DGRAM或SOCK_STREAM,protocol參數仍然指定爲0即可。
UNIX Domain Socket與網絡socket編程最明顯的不同在於地址格式不同,用結構體sockaddr_un表示,網絡編程的socket地址是IP地址加端口號,而UNIX Domain Socket的地址是一個socket類型的文件在文件系統中的路徑,這個socket文件由bind()調用創建,如果調用bind()時該文件已存在,則bind()錯誤返回。
今天我們介紹如何編寫Linux下的TCP程序,關於UDP程序可以參考這裏:
http://blog.csdn.net/htttw/article/details/7519971
本文絕大部分是參考《Linux程序設計(第4版)》的第15章套接字
服務器端的步驟如下:
1. socket: 建立一個socket
2. bind: 將這個socket綁定在某個文件上(AF_UNIX)或某個端口上(AF_INET),我們會分別介紹這兩種。
3. listen: 開始監聽
4. accept: 如果監聽到客戶端連接,則調用accept接收這個連接並同時新建一個socket來和客戶進行通信
5. read/write:讀取或發送數據到客戶端
6. close: 通信完成後關閉socket
客戶端的步驟如下:
1. socket: 建立一個socket
2. connect: 主動連接服務器端的某個文件(AF_UNIX)或某個端口(AF_INET)
3. read/write:如果服務器同意連接(accept),則讀取或發送數據到服務器端
4. close: 通信完成後關閉socket
上面是整個流程,我們先給出一個例子,具體分析會在之後給出。例子實現的功能是客戶端發送一個字符到服務器,服務器將這個字符+1後送回客戶端,客戶端再把它打印出來:
Makefile:
- all: tcp_client.c tcp_server.c
- gcc -g -Wall -o tcp_client tcp_client.c
- gcc -g -Wall -o tcp_server tcp_server.c
- clean:
- rm -rf *.o tcp_client tcp_server
tcp_server.c:
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- int main()
- {
- /* delete the socket file */
- unlink("server_socket");
- /* create a socket */
- int server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
- struct sockaddr_un server_addr;
- server_addr.sun_family = AF_UNIX;
- strcpy(server_addr.sun_path, "server_socket");
- /* bind with the local file */
- bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
- /* listen */
- listen(server_sockfd, 5);
- char ch;
- int client_sockfd;
- struct sockaddr_un client_addr;
- socklen_t len = sizeof(client_addr);
- while(1)
- {
- printf("server waiting:\n");
- /* accept a connection */
- client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
- /* exchange data */
- read(client_sockfd, &ch, 1);
- printf("get char from client: %c\n", ch);
- ++ch;
- write(client_sockfd, &ch, 1);
- /* close the socket */
- close(client_sockfd);
- }
- return 0;
- }
tcp_client.c:
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <sys/un.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <stdio.h>
- int main()
- {
- /* create a socket */
- int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
- struct sockaddr_un address;
- address.sun_family = AF_UNIX;
- strcpy(address.sun_path, "server_socket");
- /* connect to the server */
- int result = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
- if(result == -1)
- {
- perror("connect failed: ");
- exit(1);
- }
- /* exchange data */
- char ch = 'A';
- write(sockfd, &ch, 1);
- read(sockfd, &ch, 1);
- printf("get char from server: %c\n", ch);
- /* close the socket */
- close(sockfd);
- return 0;
- }
如果我們首先運行tcp_client,會提示沒有這個文件:
因爲我們是以AF_UNIX方式進行通信的,這種方式是通過文件來將服務器和客戶端連接起來的,因此我們應該先運行tcp_server,創建這個文件,默認情況下,這個文件會創建在當前目錄下,並且第一個s表示它是一個socket文件:
程序運行的結果如下圖:
下面我們詳細講解:
1.
我們調用socket函數創建一個socket:
int socket(int domain, int type, int protocol)
domain:指定socket所屬的域,常用的是AF_UNIX或AF_INET
AF_UNIX表示以文件方式創建socket,AF_INET表示以端口方式創建socket(我們會在後面詳細講解AF_INET)
type:指定socket的類型,可以是SOCK_STREAM或SOCK_DGRAM
SOCK_STREAM表示創建一個有序的,可靠的,面向連接的socket,因此如果我們要使用TCP,就應該指定爲SOCK_STREAM
SOCK_DGRAM表示創建一個不可靠的,無連接的socket,因此如果我們要使用UDP,就應該指定爲SOCK_DGRAM
protocol:指定socket的協議類型,我們一般指定爲0表示由第一第二兩個參數自動選擇。
socket()函數返回新創建的socket,出錯則返回-1
2.
地址格式:
常用的有兩種socket域:AF_UNIX或AF_INET,因此就有兩種地址格式:sockaddr_un和sockaddr_in,分別定義如下:
- struct sockaddr_un
- {
- sa_family_t sun_family; /* AF_UNIX */
- char sun_path[]; /* pathname */
- }
- struct sockaddr_in
- {
- short int sin_family; /* AF_INET */
- unsigned short int sin_port; /* port number */
- struct in_addr sin_addr; /* internet address */
- }
其中in_addr正是用來描述一個ip地址的:
- struct in_addr
- {
- unsigned long int s_addr;
- }
從上面的定義我們可以看出,sun_path存放socket的本地文件名,sin_addr存放socket的ip地址,sin_port存放socket的端口號。
3.
創建完一個socket後,我們需要使用bind將其綁定:
int bind(int socket, const struct sockaddr * address, size_t address_len)
如果我們使用AF_UNIX來創建socket,相應的地址格式是sockaddr_un,而如果我們使用AF_INET來創建socket,相應的地址格式是sockaddr_in,因此我們需要將其強制轉換爲sockaddr這一通用的地址格式類型,而sockaddr_un中的sun_family和sockaddr_in中的sin_family分別說明了它的地址格式類型,因此bind()函數就知道它的真實的地址格式。第三個參數address_len則指明瞭真實的地址格式的長度。
bind()函數正確返回0,出錯返回-1
4.
接下來我們需要開始監聽了:
int listen(int socket, int backlog)
backlog:等待連接的最大個數,如果超過了這個數值,則後續的請求連接將被拒絕
listen()函數正確返回0,出錯返回-1
5.
接受連接:
int accept(int socket, struct sockaddr * address, size_t * address_len)
同樣,第二個參數也是一個通用地址格式類型,這意味着我們需要進行強制類型轉化
這裏需要注意的是,address是一個傳出參數,它保存着接受連接的客戶端的地址,如果我們不需要,將address置爲NULL即可。
address_len:我們期望的地址結構的長度,注意,這是一個傳入和傳出參數,傳入時指定我們期望的地址結構的長度,如果多於這個值,則會被截斷,而當accept()函數返回時,address_len會被設置爲客戶端連接的地址結構的實際長度。
另外如果沒有客戶端連接時,accept()函數會阻塞
accept()函數成功時返回新創建的socket描述符,出錯時返回-1
6.
客戶端通過connect()函數與服務器連接:
int connect(int socket, const struct sockaddr * address, size_t address_len)
對於第二個參數,我們同樣需要強制類型轉換
address_len指明瞭地址結構的長度
connect()函數成功時返回0,出錯時返回-1
7.
雙方都建立連接後,就可以使用常規的read/write函數來傳遞數據了
8.
通信完成後,我們需要關閉socket:
int close(int fd)
close是一個通用函數(和read,write一樣),不僅可以關閉文件描述符,還可以關閉socket描述符