Linux 套接字:簡介(一)(?)

  • 伯克利版本的 UNIX 系統引入了一種新的通信工具 – 套接字接口,它是管道概念的一個擴展
  • 相關函數
/* 1 */
/* 創建套接字 socket() */
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
/* 2 */
/* 套接字地址,每個套接字域都有其自己的地址格式 sockaddr */
/* AF_UNIX 域地址格式 */
struct sockaddr_un {
	sa_family_t	sun_family;
	char		sun_path[];
};
/* AF_INET 域地址格式 */
struct sockaddr_in {
	short int			sin_family;
	unsigned short int	sin_port;
	struct in_addr		sin_addr;
};
/* in_addr 結構體 */
struct in_addr {
	unsigned long int 	s_addr;
};
/* 3 */
/* 命名套接字 bind() */
/* AF_UNIX 域套接字會關聯到一個文件系統的路徑名 */
/* AF_INET 套接字會關聯到一個 IP 端口號 */
/* bind 系統調用把參數 address 中的地址分配給與文件描述符 socket 關聯的未命名套接字 */
#include <sys/socket.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);
/* 4 */
/* 創建套接字隊列,創建一個隊列來保存未處理的請求 */
#include <sys/socket.h>
int listen(int socket, int backlog);
/* 5 */
/* 接受連接 */
#include <sys/socket.h>
int accept(int socket, struct sockaddr *address, size_t *address_len);
/* 6 */
/* 請求連接 */
#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len);
/* 7 */
/* 關閉套接字 */
int close(int fd);
  • 注意事項:1. 主機字節序 與 網絡字節序的轉換, 2. 多線程安全問題
  • 套接字的域 domain 協議族列表(查看詳情命令 ‘ man socket ’)
名稱 說明
AF_UNIX UNIX 域協議(文件系統套接字) (常用)
AF_INET ARPA 因特網協議( UNIX 網絡套接字) (常用)
AF_ISO ISO 標準協議
AF_NS 施樂(Xerox)網絡系統協議
AF_IPX Novell IPX 協議
AF_APPLETALK Appletalk DDS

目錄
  1. 一個 簡單的 本地客戶/服務器 demo(逐個處理客戶端請求 與 多線程同時處理多個客戶端請求)
  2. 一個 使用 select 系統調用 的 客戶/服務器 demo(同時處理多個客戶端請求)
    服務器通過同時在多個打開的套接字上等待請求到來的方法來處理多個客戶端連接。

1. 一個簡單的本地客戶/服務器 demo
  1. 客戶端,代碼如下:
/* sc1.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void main(){
    int sockfd;
    int len;
    struct sockaddr_un address;
    int res;
    char ch = 'A';
	// 新建一個套接字
    assert((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) != -1);
    address.sun_family = AF_UNIX; // UNIX 域套接字
    strcpy(address.sun_path, "server_socket");
    len = sizeof(address);
    // 連接到服務器
    assert((res = connect(sockfd, (struct sockaddr *)&address, len)) == 0);

    assert(write(sockfd, &ch, 1) == 1); // 向服務器發送一個字節的數據
    assert(read(sockfd, &ch, 1) == 1); // 讀取服務器端的響應
    fprintf(stdout, "client received: %c\n", ch);
}
  1. 服務器端代碼如下:
/* ss1.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>

void *thread_handle_client_conn(void *arg);

void main(){
    int server_sock_fd, client_sock_fd;
    int server_len, client_len;
    struct sockaddr_un server_address;
    struct sockaddr_un client_address;
    char ch;

    unlink("server_socket");
    // 新建一個套接字
    assert((server_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0)) != -1);
    server_address.sun_family = AF_UNIX;
    strcpy(server_address.sun_path, "server_socket");
    server_len = sizeof(server_address);
	// 命名套接字
    assert(bind(server_sock_fd, (struct sockaddr *)&server_address, server_len) == 0);
	// 創建套接字隊列
    assert(listen(server_sock_fd, 5) == 0);

    while(1){
        fprintf(stdout, "server waiting for client ...\n");
        client_len = sizeof(client_address);
        // accept client conn(接受客戶端連接)
        assert((client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &client_len)) != -1);
        // handle client conn(處理客戶端連接)
        //
        // 方式 1, 服務器逐個處理客戶端連接請求
        // assert(read(client_sock_fd, &ch, 1) == 1); // 讀取客戶端發送來的數據
        // fprintf(stdout, "server received: %c\n", ch);
        // sleep(5);
        // ch++;
        // assert(write(client_sock_fd, &ch, 1) == 1); // 寫入數據,響應客戶端
        // close(client_sock_fd);

        //
        // 方式 2, 每個客戶端連接使用一個單獨線程處理,
        // 使用下面 2 行取代 ‘int i = client_sock_fd;’語句,
        // 當涉及多線程編程時,傳遞指針要尤其注意。
        int *i = (int *)malloc(sizeof(int));
        *i = client_sock_fd;
        pthread_t thread_client;
        assert(pthread_create(&thread_client, NULL, thread_handle_client_conn, i) == 0);
    }
}

void *thread_handle_client_conn(void *arg){
    char ch;
    int client_sock_fd = *((int *)arg);

    assert(read(client_sock_fd, &ch, 1) == 1); // 讀取客戶端發送來的數據
    fprintf(stdout, "server received: %c\n", ch);
    sleep(5);
    ch++;
    assert(write(client_sock_fd, &ch, 1) == 1); // 寫入數據,響應客戶端
    close(client_sock_fd);
    free(arg); // 釋放 malloc 分配的內存資源
    pthread_exit(NULL);
}

編譯,並運行服務器程序:

ubuntu@cuname:~/dev/beginning-linux-programming/test$ gcc -o ss1.1 ss1.1.c -lpthread
ubuntu@cuname:~/dev/beginning-linux-programming/test$ ./ss1.1
server waiting for client ...

同時啓動多個客戶端(簡單 shell 腳本 sc1_many.sh):

#!/bin/bash
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
do
    ./sc1 &
done
exit 0

輸出結果(執行腳本後,等待 5 秒,同時輸出服務器返回結果):

ubuntu@cuname:~/dev/beginning-linux-programming/test$ ./sc1_many.sh
ubuntu@cuname:~/dev/beginning-linux-programming/test$ client received: B
client received: B
client received: B
...
client received: B
client received: B

2. 使用 select 系統調用的客戶/服務器 demo(同時處理多個客戶端請求)
  • 服務器通過 select 函數調用,同時在多個打開的套接字上等待請求到來並處理多個客戶端連接
  1. 客戶端代碼如下:
/* sc2.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

void main(){
    int sockfd;
    int len;
    struct sockaddr_un address;
    int res;
    char ch = 'A';

    assert((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) != -1);
    address.sun_family = AF_UNIX;
    strcpy(address.sun_path, "server_socket");
    len = sizeof(address);
    
    assert((res = connect(sockfd, (struct sockaddr *)&address, len)) == 0);
    sleep(5); // 先睡眠 5 秒
    assert(write(sockfd, &ch, 1) == 1);
    assert(read(sockfd, &ch, 1) == 1);
    fprintf(stdout, "client received: %c\n", ch);
}
  1. 服務器端代碼如下:
  • 在調用 select 函數時,
  1. 一定要 避免添加無效的描述符
  2. 也要 避免重複添加
  3. 每次調用完 select 之後,在再次調用之前,必須 把感興趣的描述符再次添加 到對應的集合中
/* ss2.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
#include <sys/time.h>

// 最大客戶端連接數目
#define MAX_CLIENT_NUM 500

void main(){
    int server_sock_fd, client_sock_fd;
    int server_len, client_len;
    struct sockaddr_un server_address;
    struct sockaddr_un client_address;
    char ch;
    int res;
    // 查看 man 手冊 ‘man select’
    // 該變量用於 select 函數調用的第一個參數,含義如下:
    // nfds is the highest-numbered file descriptor in any of the three sets, plus 1.
    int max_fd_plus_one;
    // 簡單用於保存客戶端連接的 socket 描述符
    int fd_clients[MAX_CLIENT_NUM];
    // init,使用 -1 初始化數組中的每一個元素
    for(int i = 0;i < MAX_CLIENT_NUM;i++){
        fd_clients[i] = -1;
    }
    // 統計客戶端數目,只增不減,必須注意數組下標越界!
    int fd_clients_count = 0;

    fd_set readfds;
    FD_ZERO(&readfds);

    unlink("server_socket");
    assert((server_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0)) != -1);
    max_fd_plus_one = server_sock_fd + 1; // 初始化
    FD_SET(server_sock_fd, &readfds); // 監聽服務器套接字
    server_address.sun_family = AF_UNIX;
    strcpy(server_address.sun_path, "server_socket");
    server_len = sizeof(server_address);

    assert(bind(server_sock_fd, (struct sockaddr *)&server_address, server_len) == 0);

    assert(listen(server_sock_fd, 5) == 0);

    fprintf(stdout, "server waiting for client...\n");
    while(1){
        switch(res = select(max_fd_plus_one, &readfds, NULL, NULL, NULL)){
            case 0:
                continue;
            case -1:
                // error
                fprintf(stdout, "error: select failed.\n");
                exit(EXIT_FAILURE);
            default:
                if(FD_ISSET(server_sock_fd, &readfds)){
                    // server fd
                    // 無阻塞調用 accept 函數,獲取客戶端連接
                    client_len = sizeof(client_address);
                    assert((client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &client_len)) != -1);
                    // 更新
                    max_fd_plus_one = (client_sock_fd > max_fd_plus_one ? client_sock_fd + 1 : max_fd_plus_one);
                    // 添加客戶端 socket 描述符到 fd_clients 數組
                    fd_clients[fd_clients_count++] = client_sock_fd;
                }else{
                    // retry add server fd
                    // 將服務器端連接描述符再次添加到 readfds 中,供 select 函數監聽可讀操作
                    FD_SET(server_sock_fd, &readfds);
                }
                // handle client
                for(int i = 0;i < fd_clients_count;i++){
                    int c_fd = fd_clients[i];
                    if (c_fd != -1 && FD_ISSET(c_fd, &readfds)){
                        assert(read(c_fd, &ch, 1) == 1);
                        fprintf(stdout, "server received: %c, max_client_fd:%d\n", ch, max_fd_plus_one - 1);
                        ch++;
                        assert(write(c_fd, &ch, 1) == 1);
                        // 移除已經處理完畢的描述符
                        FD_CLR(c_fd, &readfds);
                        // remove c_fd from fd_clients
                        // 客戶端請求處理完畢後,將其從 fd_clients 中移除
                        for(int k = 0;k < fd_clients_count;k++){
                            if(fd_clients[k] == c_fd){
                                fd_clients[k] = -1;
                                break;
                            }
                        }
                        close(c_fd);
                    }
                }
                // 將客戶端連接描述符再次添加到 readfds 中,供 select 函數監聽可讀操作
                // 一定要避免添加無效的描述符
                // 準備繼續下一次監聽
                for(int j = 0;j < fd_clients_count;j++){
                    if(fd_clients[j] != -1 && !FD_ISSET(fd_clients[j], &readfds)){
                        FD_SET(fd_clients[j], &readfds);
                    }
                }
        }
    }
}

多客戶端腳本(sc2_many.sh):

#!/bin/bash
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
do
    ./sc2 &
done
exit 0

執行情況:

  • 運行結果達到預期,服務器端可以快速同時處理多個客戶端請求,
  • 異常(有時):當連續快速執行 2 次客戶端 shell 腳本,即同時創建了 40 個客戶端,卻只有 39 個響應輸出,然後查看了一下進程信息,發現原來有 1 個客戶端在 ‘睡覺’! (哈哈)(不知道是不是我機器原因,還是程序問題,也不排除 shell 腳本連續啓動太多客戶端的原因,歡迎反饋)(我想可能是程序問題,服務器端在處理客戶端請求時,並沒有監聽對客戶端的寫操作,對於客戶端的讀和寫可以分別放入對應的獨立集合中進行監聽,當然這就必須把對 readsfd 的類似操作再複製一遍到新增的 writesfd 集合中,然後將寫入操作挪過來即可)
    有一個客戶端在睡覺
    我的虛擬機配置如下(使用 VMware ):
    我的虛擬機配置
  1. 嘗試解決上述 3 中的問題,(按照 3 中的思路)修改服務器端代碼如下(將讀寫操作分開,服務器端在完成讀取客戶端數據之後,纔會將客戶端 socket 描述符加入寫監聽集合,在寫操作完成後,客戶端連接終止):
  • 發現這個問題還是存在,查看進程信息(即使將 listen 函數的套接字隊列增大到 50,還是有這種情況。我也很絕望啊)(該客戶端進程阻塞在 read 函數,所以說明 read 函數前面的 write 函數執行成功,但是服務器端的輸出也是隻有 39 行,即服務器端並沒有接收到該客戶端的請求數據)(疑點重重,這個問題先放放)
ubuntu@cuname:~/dev/beginning-linux-programming/test$ ps ax -O wchan:22 | grep sc2
 39271 unix_stream_read_gener S pts/19   00:00:00 ./sc2
 39351 pipe_wait              S pts/19   00:00:00 grep --color=auto sc2
/* ss3.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
#include <sys/time.h>

// 最大客戶端連接數目
#define MAX_CLIENT_NUM 1000

void main(){
    int server_sock_fd, client_sock_fd;
    int server_len, client_len;
    struct sockaddr_un server_address;
    struct sockaddr_un client_address;
    char ch;
    int res;
    // 查看 man 手冊 ‘man select’
    // 該變量用於 select 函數調用的第一個參數,含義如下:
    // nfds is the highest-numbered file descriptor in any of the three sets, plus 1.
    int max_fd_plus_one;
    // 簡單用於保存客戶端連接的 socket 描述符
    int fd_clients_read[MAX_CLIENT_NUM];
    int fd_clients_write[MAX_CLIENT_NUM];
    // init,使用 -1 初始化數組中的每一個元素
    for(int i = 0;i < MAX_CLIENT_NUM;i++){
        fd_clients_read[i] = -1;
        fd_clients_write[i] = -1;
    }
    // 統計客戶端數目,只增不減,必須注意數組下標越界!
    int fd_clients_count_read = 0;
    int fd_clients_count_write = 0;

    fd_set readfds;
    fd_set writefds;
    FD_ZERO(&writefds);
    FD_ZERO(&readfds);

    unlink("server_socket");
    assert((server_sock_fd = socket(AF_UNIX, SOCK_STREAM, 0)) != -1);
    max_fd_plus_one = server_sock_fd + 1; // 初始化
    FD_SET(server_sock_fd, &readfds); // 監聽服務器套接字
    server_address.sun_family = AF_UNIX;
    strcpy(server_address.sun_path, "server_socket");
    server_len = sizeof(server_address);

    assert(bind(server_sock_fd, (struct sockaddr *)&server_address, server_len) == 0);

    assert(listen(server_sock_fd, 5) == 0);

    fprintf(stdout, "server waiting for client...\n");
    while(1){
        switch(res = select(max_fd_plus_one, &readfds, &writefds, NULL, NULL)){
            case 0:
                continue;
            case -1:
                // error
                fprintf(stdout, "error: select failed.\n");
                exit(EXIT_FAILURE);
            default:
                if(FD_ISSET(server_sock_fd, &readfds)){
                    // server fd
                    // 無阻塞調用 accept 函數,獲取客戶端連接
                    client_len = sizeof(client_address);
                    assert((client_sock_fd = accept(server_sock_fd, (struct sockaddr *)&client_address, &client_len)) != -1);
                    // 更新
                    max_fd_plus_one = (client_sock_fd > max_fd_plus_one ? client_sock_fd + 1 : max_fd_plus_one);
                    // 添加客戶端 socket 描述符到 fd_clients 數組
                    fd_clients_read[fd_clients_count_read++] = client_sock_fd;
                }else{
                    // retry add server fd
                    // 將服務器端連接描述符再次添加到 readfds 中,供 select 函數監聽可讀操作
                    FD_SET(server_sock_fd, &readfds);
                }
                // handle client for read
                for(int i = 0;i < fd_clients_count_read;i++){
                    int c_fd = fd_clients_read[i];
                    if (c_fd != -1 && FD_ISSET(c_fd, &readfds)){
                        // read from client
                        assert(read(c_fd, &ch, 1) == 1);
                        fprintf(stdout, "server received: %c, max_client_fd:%d\n", ch, max_fd_plus_one - 1);
                        // 移除已經處理完畢的描述符
                        FD_CLR(c_fd, &readfds);
                        // remove c_fd from fd_clients
                        // 客戶端請求處理完畢後,將其從 fd_clients 中移除
                        for (int k = 0; k < fd_clients_count_read; k++){
                            if (fd_clients_read[k] == c_fd){
                                fd_clients_read[k] = -1;
                                break;
                            }
                        }
                        // for write
                        fd_clients_write[fd_clients_count_write++] = c_fd;
                    }
                }
                // handle client for write
                for(int k = 0;k < fd_clients_count_write;k++){
                    int c_fd = fd_clients_write[k];
                    if(c_fd != -1 && FD_ISSET(c_fd, &writefds)){
                            // write to client
                            assert(write(c_fd, "B", 1) == 1);
                            // 移除已經處理完畢的描述符
                            FD_CLR(c_fd, &writefds);
                            // remove c_fd from fd_clients
                            // 客戶端請求處理完畢後,將其從 fd_clients 中移除
                            for (int k = 0; k < fd_clients_count_write; k++){
                                if (fd_clients_write[k] == c_fd){
                                    fd_clients_write[k] = -1;
                                    break;
                                }
                            }
                            close(c_fd); // client is over.
                        }
                }
                // 將客戶端連接描述符再次添加到 readfds 中,供 select 函數監聽可讀操作
                // 一定要避免添加無效的描述符
                // 準備繼續下一次監聽
                for(int j = 0;j < fd_clients_count_read;j++){
                    if(fd_clients_read[j] != -1 && !FD_ISSET(fd_clients_read[j], &readfds)){
                        FD_SET(fd_clients_read[j], &readfds);
                    }
                }
                for(int j = 0;j < fd_clients_count_write;j++){
                    if(fd_clients_write[j] != -1 && !FD_ISSET(fd_clients_write[j], &writefds)){
                        FD_SET(fd_clients_write[j], &writefds);
                    }
                }
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章