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

  • 伯克利版本的 UNIX 系统引入了一种新的通信工具 – 套接字接口,它是管道概念的一个扩展
  • 相关函数
/* 1 */
/* 创建套接字 socket() */
#include 
#include 
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 
int bind(int socket, const struct sockaddr *address, size_t address_len);
/* 4 */
/* 创建套接字队列,创建一个队列来保存未处理的请求 */
#include 
int listen(int socket, int backlog);
/* 5 */
/* 接受连接 */
#include 
int accept(int socket, struct sockaddr *address, size_t *address_len);
/* 6 */
/* 请求连接 */
#include 
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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 最大客户端连接数目
#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 集合中,然后将写入操作挪过来即可)
    Linux 套接字:简介(一)(?)_第1张图片
    我的虚拟机配置如下(使用 VMware ):
    Linux 套接字:简介(一)(?)_第2张图片
  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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

// 最大客户端连接数目
#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);
                    }
                }
        }
    }
}

你可能感兴趣的:(C,Linux,-,程序设计,Unix/Linux)