Unix Socket編程入門

不管是做哪一種開發,都離不開網絡編程,網絡編程又經常會涉及套接字(socket)。Socket最初由加州大學伯克利分校開發,它是一種全雙工的通信方式,不同於pipe這種單工方式,主要用於實現4.2BSD上的進程間通信。

我們常說的socket通信有以下二種,主要會說一下Unix domain socket

Internet domain socket

也叫IP socket,它要利用主機的傳輸層(tcp),可以用於同一臺主機上不同進程間的通信,也可以用於網絡上不同主機間的通信。就像聊QQ一樣只要知道了對方的QQ號就可以聊天了。socket只要知道了對方的ip地址和端口就可以通信了所以這種socket通信是基於網絡協議棧的。

Unix domain socket

Unix domain socket,也叫IPC socket(inter-precess communication socket,也就是進程間通信套接字),用於同一臺主機上的不同進程間交換數據,是Posix系統的標準組件。
該socket用於一臺主機的進程間通信,不需要基於網絡協議,主要是基於文件系統的。與Internet domain socket類似,需要知道是基於哪一個文件(相同的文件路徑)來通信的。unix domain socket有2種工作模式一種是SOCK_STREAM,類似於TCP,可靠的字節流。另一種是SOCK_DGRAM,類似於UDP,不可靠的字節流。除了傳輸數據以外,還可以使用Unix domain socket傳輸文件描述符(file descriptor)。


工作模型

socket通信有一個服務端,一個客服端
服務端:創建socket—綁定文件(端口)—監聽—接受客戶端連接—接收/發送數據—…—關閉
客戶端:創建socket—綁定文件(端口)—連接—發送/接收數據—…—關閉

代碼框架

server

sockaddr_un :為一個系統級的結構體,主要用於存儲地址

#define NAME "socketAddr"
main(){
...
struct sockaddr_un server;
...
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
strcpy (server.sun_path, NAME);
...
bind(sock, (structsockaddr *) &server, sizeof(struct sockaddr_un));
...
listen(sock, 5);
...
while(1){
...
msgsock = accept(sock, 0, 0);
...
rval = read(msgsock, buf, 1024))
...
}
}

client

#define NAME "socketAddr"
main(){
...

sock = socket(AF_UNIX, SOCK_STREAM, 0);
strcpy(server.sun_path, NAME);
...
if (connect(sock, (struct sockaddr *) &server, sizeof(struct sockaddr_un)) < 0) {
close(sock);
exit(1);
}
if (write(sock, DATA, sizeof(DATA)) < 0)
perror("writing on stream socket");
close(sock);
}


使用:

  1. gcc service.c -o service
  2. gcc client.c -o client
  3. linux下啟動一個窗口運行 ./service
  4. 啟動另一個窗口運行 ./client socket 【這個socket為service中綁定的地址】
    運行後可以發現,在當前目錄下會多一個名叫socket的文件,這2個進程就是基於該文件通信的

Unix domain socket vs Internet domain socket

先來看一個使用案例,配置php-fpm與Nginx交互的socket:

fastcgi_pass 127.0.0.1:9000

fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock

這個案例中,運行在同一臺機器上的php和Nginx需要通信,有2種實現方式:第一種是ip socket,通過本機迴環地址127.0.0.1加端口實現;第二種是通過unix domain socket實現。哪一種效率更高呢?

基於localhost的ip socket需要實現跨網絡主機通訊的全部環節,包括建立socket連接,ACk開銷,tcp流控,封裝/解封,路由。在這個過程中還會有2個context switch,因為使用網絡層傳輸數據需要調用system call,而調用system call會產生中斷,導致context switch的;另外一個進程接受到來自網絡層的連接請求,也會產生系統中斷,導致context switch。以上過程導致2個context switch的開銷,外加其它各種開銷(overhead)。

如何通過命令行訪問unix socket文件

socket為什麼不能用傳統命令訪問?

socket文件不能通過普通的文件讀寫命令操作(比如說echo "xxx" > socket.file)它。因為它是在網絡層上面工作的。只能通過socket讀寫函數去操作它。

socat和ncat命令

其實通過的linux命令socat和ncat可以去操作socket。

其中-U指定了該文件是Unix域socket文件類型,ncat實現了類似於cat命令的訪問unix socket。

ncat -U /tmp/tbsocket1

ncat也可以通過映射socket文件到監聽的端口上。那麼通過curl可以發送請求到該監聽端口,實現寫操作。

# 映射tcp的8080流量到unix socket

ncat -vlk 8080 -c 'ncat -U /tmp/tbsocket1'

# 通過curl發起http請求訪問

curl http://localhost:8080

也可以使用功能更強大的socat來實現。

# 映射8080/tcp 到unix socket

socat -d -d TCP-LISTEN:8080,fork UNIX:/tmp/tbsocket1

用 Curl 命令訪問 Unix Socket 接口的方法

經常遇到一些監聽地址不是 IP:Port 而是 unix socket 的程序,這些程序如果使用的是 HTTP 協議,unix socket 接口也可以用 curl 訪問。

例如 ingress-nginx 的監聽地址為 unix:/tmp/nginx-status-server.sock:

server {
listen unix:/tmp/nginx-status-server.sock;
set $proxy_upstream_name "internal";

keepalive_timeout 0;
gzip off;

access_log off;

location /healthz {
return 200;
}

location /nginx_status {
stub_status on;
}
... 省略...
}

用 curl 訪問它的 unix socket 的方法如下

$ curl --unix-socket /tmp/nginx-status-server.sock http://localhost/nginx_status
Active connections: 77
server accepts handled requests
64273 64273 971368
Reading: 0 Writing: 12 Waiting: 65

--unix-socket 指定 unix socket 文件的地址, http://localhost/nginx_status 是要請求的路徑。

注意 localhost 可以根據實際情況更改成其它數值但不可省略,如果省略後變成 http://nginx_status,那麼 nginx_status 會被認作是 Host,Path 被認為是 /:

$ curl -v --unix-socket /tmp/nginx-status-server.sock http://nginx_status
* Expire in 0 ms for 6 (transfer 0xe464ab3dd0)
* Trying /tmp/nginx-status-server.sock...
* Expire in 200 ms for 4 (transfer 0xe464ab3dd0)
* Connected to nginx_status (/tmp/nginx-status-server.sock) port 80 (#0)
> GET / HTTP/1.1
> Host: nginx_status
> User-Agent: curl/7.64.0
> Accept: */*


分享到:


相關文章: