Linux socket本地程序間通訊
使用套接字除了可以實現網路間不同主機間的通訊外,還可以實現同一主機的不同程序間的通訊,且建立的通訊是雙向的通訊。socket程序通訊與網路通訊使用的是統一套介面,只是地址結構與某些引數不同。
一、建立socket流程
(1)建立socket,型別為AF_LOCAL或AF_UNIX,表示用於程序通訊:
建立套接字需要使用 socket 系統呼叫,其原型如下:
int socket(int domain, int type, int protocol);
其中,domain 引數指定協議族,對於本地套接字來說,其值須被置為 AF_UNIX 列舉值;type 引數指定套接字型別,protocol 引數指定具體協議;type 引數可被設定為 SOCK_STREAM(流式套接字)或 SOCK_DGRAM(資料報式套接字),protocol 欄位應被設定為 0;其返回值為生成的套接字描述符。
對於本地套接字來說,流式套接字(SOCK_STREAM)是一個有順序的、可靠的雙向位元組流,相當於在本地程序之間建立起一條資料通道;資料報式套接字(SOCK_DGRAM)相當於單純的傳送訊息,在程序通訊過程中,理論上可能會有資訊丟失、複製或者不按先後次序到達的情況,但由於其在本地通訊,不通過外界網路,這些情況出現的概率很小
二、命名socket
SOCK_STREAM 式本地套接字的通訊雙方均需要具有本地地址,其中伺服器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 型別的變數。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 路徑名 */
};
這裡面有一個很關鍵的東西,socket程序通訊命名方式有兩種。一是普通的命名,socket會根據此命名建立一個同名的socket檔案,客戶端連線的時候通過讀取該socket檔案連線到socket服務端。這種方式的弊端是服務端必須對socket檔案的路徑具備寫許可權,客戶端必須知道socket檔案路徑,且必須對該路徑有讀許可權。
另外一種命名方式是抽象名稱空間,這種方式不需要建立socket檔案,只需要命名一個全域性名字,即可讓客戶端根據此名字進行連線。後者的實現過程與前者的差別是,後者在對地址結構成員sun_path陣列賦值的時候,必須把第一個位元組置0,即sun_path[0] = 0,下面用程式碼說明:
第一種方式:
- //name the server socket
- server_addr.sun_family = AF_UNIX;
- strcpy(server_addr.sun_path,"/tmp/UNIX.domain");
- server_len = sizeof(struct sockaddr_un);
- client_len = server_len;
第二種方式:
- #define SERVER_NAME @socket_server
- //name the socket
- server_addr.sun_family = AF_UNIX;
- strcpy(server_addr.sun_path, SERVER_NAME);
- server_addr.sun_path[0]=0;
- //server_len = sizeof(server_addr);
- server_len = strlen(SERVER_NAME) + offsetof(struct sockaddr_un, sun_path);
其中,offsetof函式在#include <stddef.h>標頭檔案中定義。因第二種方式的首位元組置0,我們可以在命名字串SERVER_NAME前新增一個佔位字串,例如:
- #define SERVER_NAME @socket_server
前面的@符號就表示佔位符,不算為實際名稱。
提示:客戶端連線伺服器的時候,必須與服務端的命名方式相同,即如果服務端是普通命名方式,客戶端的地址也必須是普通命名方式;如果服務端是抽象命名方式,客戶端的地址也必須是抽象命名方式。
三、繫結
SOCK_STREAM 式本地套接字的通訊雙方均需要具有本地地址,其中伺服器端的本地地址需要明確指定,指定方法是使用 struct sockaddr_un 型別的變數,將相應欄位賦值,再將其繫結在建立的伺服器套接字上,繫結要使用 bind 系統呼叫,其原形如下:
int bind(int socket, const struct sockaddr *address, size_t address_len);
其中表示伺服器端的套接字描述符,address 表示需要繫結的本地地址,是一個 struct sockaddr_un 型別的變數,address_len 表示該本地地址的位元組長度。實現伺服器端地址指定功能的程式碼如下(假設伺服器端已經通過上文所述的
socket 系統呼叫建立了套接字,server_sockfd 為其套接字描述符):
struct sockaddr_un server_address;
server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, "Server Socket");
bind(server_sockfd, (struct sockaddr*)&server_address, sizeof(server_address));
客戶端的本地地址不用顯式指定,只需能連線到伺服器端即可,因此,客戶端的 struct sockaddr_un 型別變數需要根據伺服器的設定情況來設定,程式碼如下(假設客戶端已經通過上文所述的 socket 系統呼叫建立了套接字,client_sockfd 為其套接字描述符):
struct sockaddr_un client_address;
client_address.sun_family = AF_UNIX;
strcpy(client_address.sun_path, "Server Socket");
四、監聽
伺服器端套接字建立完畢並賦予本地地址值(名稱,本例中為Server Socket)後,需要進行監聽,等待客戶端連線並處理請求,監聽使用 listen 系統呼叫,接受客戶端連線使用accept系統呼叫,它們的原形如下:
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr *address, size_t *address_len);
其中 socket 表示伺服器端的套接字描述符;backlog 表示排隊連線佇列的長度(若有多個客戶端同時連線,則需要進行排隊);address 表示當前連線客戶端的本地地址,該引數為輸出引數,是客戶端傳遞過來的關於自身的資訊;address_len 表示當前連線客戶端本地地址的位元組長度,這個引數既是輸入引數,又是輸出引數。實現監聽、接受和處理的程式碼如下:
#define MAX_CONNECTION_NUMBER 10
int server_client_length, server_client_sockfd;
struct sockaddr_un server_client_address;
listen(server_sockfd, MAX_CONNECTION_NUMBER);
while(1)
{
// ...... (some process code)
server_client_length = sizeof(server_client_address);
server_client_sockfd = accept(server_sockfd, (struct sockaddr*)&server_client_address, &server_client_length);
// ...... (some process code)
}
這裡使用死迴圈的原因是伺服器是一個不斷提供服務的實體,它需要不間斷的進行監聽、接受並處理連線,本例中,每個連線只能進行序列處理,即一個連線處理完後,才能進行後續連線的處理。如果想要多個連線併發處理,則需要建立執行緒,將每個連線交給相應的執行緒併發處理。
客戶端套接字建立完畢並賦予本地地址值後,需要連線到伺服器端進行通訊,讓伺服器端為其提供處理服務。對於 SOCK_STREAM 型別的流式套接字,需要客戶端與伺服器之間進行連線方可使用。連線要使用 connect 系統呼叫,其原形為
int connect(int socket, const struct sockaddr *address, size_t address_len);
其中socket為客戶端的套接字描述符,address表示當前客戶端的本地地址,是一個 struct sockaddr_un 型別的變數,address_len 表示本地地址的位元組長度。實現連線的程式碼如下:
connect(client_sockfd, (struct sockaddr*)&client_address, sizeof(client_address));
無論客戶端還是伺服器,都要和對方進行資料上的互動,這種互動也正是我們程序通訊的主題。一個程序扮演客戶端的角色,另外一個程序扮演伺服器的角色,兩個程序之間相互發送接收資料,這就是基於本地套接字的程序通訊。傳送和接收資料要使用 write 和 read 系統呼叫,它們的原形為:
int read(int socket, char *buffer, size_t len);
int write(int socket, char *buffer, size_t len);
其中 socket 為套接字描述符;len 為需要傳送或需要接收的資料長度;對於 read 系統呼叫,buffer 是用來存放接收資料的緩衝區,即接收來的資料存入其中,是一個輸出引數;對於 write 系統呼叫,buffer 用來存放需要傳送出去的資料,即 buffer 內的資料被髮送出去,是一個輸入引數;返回值為已經發送或接收的資料長度。例如客戶端要傳送一個 "Hello" 字串給伺服器,則程式碼如下:
char buffer[10] = "Hello";
write(client_sockfd, buffer, strlen(buffer));
互動完成後,需要將連線斷開以節省資源,使用close系統呼叫,其原形為:
int close(int socket);
不多說了,直接使用,大家一定都會,呵呵!
上面所述的每個系統呼叫都有 -1 返回值,在呼叫不成功時,它們均會返回 -1,這個特性可以使得我們用 if - else 或異常處理語句來處理錯誤,為我們提供了很大的方便。
SOCK_DGRAM 資料報式本地套接字的應用場合很少,因為流式套接字在本地的連線時間可以忽略,所以效率並沒有提高,而且傳送接收都需要攜帶對方的本地地址,因此很少甚至幾乎不使用。
與本地套接字相對應的是網路套接字,可以用於在網路上傳送資料,換言之,可實現不同機器上的程序通訊過程。在 TCP/IP 協議中,IP 地址的首位元組為 127 即代表本地,因此本地套接字通訊可以使用 IP 地址為 127.x.x.x 的網路套接字來實現。