学而实习之 不亦乐乎

Linux Socket 编程基础

2023-10-27 07:47:33

一台计算机系统内的进程间通信,可以通过共享资源实现,如文件系统空间、共享的物理内存或消息队列,但都只是运行在同一台机器上的进程才能使用它们。UNIX系统引入了一种新的通信工具:套接字接口(socket interface),它是管道概念的一个扩展。Linux系统支持套接字接口。

你可以通过与使用管道类似的方法来使用套接字,但套接字还包括了计算机网络中的通信。一台机器上的进程可以使用套接字和另外一台机器上的进程通信,这样就可以支持分布在网络中的客户/服务器系统。同一台机器上的进程之间也可以使用套接字进行通信。

此外,Windows 系统也通过可公开获取的 Windows Sockets 技术规范(简称WinSock)实现了套接字接口。Windows系统的套接字服务是由系统文件 winsock.dll 来提供的。因此,Windows 程序可以通过网络和 Linux/UNIX 计算机进行通信来实现客户/服务器系统,反之亦然。虽然WinSock的编程接口和UNIX套接字不尽相同,但它同样是以套接字为基础的。Linux 有着丰富的网络功能。

一、什么是套接字

套接字 (socket)是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行。Linux 所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(如用于远程登录的 rlogin 和用于文件传输的 ftp)通常都是通过套接字来进行通信的。套接字的创建和使用与管道是有区别的,因为套接字明确地将客户和服务器区分开来。套接字机制可以实现将多个客户连接到一个服务器。

二、套接字连接

你可以把套接字连接想象为打电话进一个繁忙的办公大楼。一个电话打到一家公司,接线员接听电话并把它转到正确的部门(服务器进程),然后再从那里转到电话要找的人(服务器套接字)。每个进入的电话呼叫(客户)都被转到正确的终端节点,而中间介入的接线员则可以空出来处理后续的电话。

套接字应用程序是如何通过套接字来维持一个连接的?

1、首先,服务器应用程序用系统调用 socket 来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他进程共享。
2、接下来,服务器进程会给套接字起个名字。本地套接字的名字是 Linux 文件系统中的文件名,一般放在 /tmp 或 /usr/tmp 目录中。对于网络套接字,它的名字是与客户连接的特定网络有关的服务标识符(端口号或访问点)。这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。
例如,Web服务器一般在 80 端口上创建一个套接字,这是一个专用于此目的的标识符。Web浏览器知道对于用户想要访问的 Web 站点,应该使用端口 80 来建立 HTTP 连接。我们用系统调用 bind 来给套接字命名。然后服务器进程就开始等待客户连接到这个命名套接字。系统调用 listen 的作用是,创建一个队列并将其用于存放来自客户的进入连接。服务器通过系统调用 accept 来接受客户的连接。服务器调用 accept 时,它会创建一个与原有的命名套接字不同的新套接字。这个新套接字只用于与这个特定的客户进行通信,而命名套接字则被保留下来继续处理来自其他客户的连接。如果服务器编写得当,它就可以充分利用多个连接带来的好处。Web服务器就会这么做以同时服务来自许多客户的页面请求。对一个简单的服务器来说,后续的客户将在监听队列中等待,直到服务器再次准备就绪。

基于套接字系统的客户端更加简单。客户首先调用 socket 创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用 connect 与服务器建立连接。一旦连接建立,就可以像使用底层的文件描述符那样用套接字来实现双向的数据通信。

三、实例

一个简单的本地客户

1、客户端程序

下面是一个非常简单的套接字客户程序的 client1.c。它创建一个未命名的套接字,然后把它连接到服务器套接字server_socket。

/* 1. Make the necessary includes and set up the variables.  */

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>

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

/* 2. Create a socket for the client.  */

    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

/* 3. Name the socket, as agreed with the server.  */

    address.sun_family = AF_UNIX;
    strcpy(address.sun_path, "server_socket");
    len = sizeof(address);

/* 4. Now connect our socket to the server's socket.  */

    result = connect(sockfd, (struct sockaddr *)&address, len);

    if(result == -1) {
        perror("oops: client1");
        exit(1);
    }

/* 5. We can now read/write via sockfd.  */

    write(sockfd, &ch, 1);
    read(sockfd, &ch, 1);
    printf("char from server = %c\n", ch);
    close(sockfd);
    exit(0);
}

2、服务器端

下面是一个非常简单的服务器程序 server1.c,它接受来自客户程序的连接。它首先创建一个服务器套接字,将它绑定到一个名字,然后创建一个监听队列,开始接受客户的连接。

/* 1. Make the necessary includes and set up the variables.  */

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_un server_address;
    struct sockaddr_un client_address;

/* 2. Remove any old socket and create an unnamed socket for the server.  */

    unlink("server_socket");
    server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

/* 3. Name the socket.  */

    server_address.sun_family = AF_UNIX;
    strcpy(server_address.sun_path, "server_socket");
    server_len = sizeof(server_address);
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

/* 4. Create a connection queue and wait for clients.  */

    listen(server_sockfd, 5);
    while(1) {
        char ch;

        printf("server waiting\n");

/* 5. Accept a connection.  */

        client_len = sizeof(client_address);
        client_sockfd = accept(server_sockfd, 
            (struct sockaddr *)&client_address, &client_len);

/* 6. We can now read/write to client on client_sockfd.  */

        read(client_sockfd, &ch, 1);
        ch++;
        write(client_sockfd, &ch, 1);
        close(client_sockfd);
    }
}

3、解析

这个例子中的服务器程序一次只能为一个客户服务。它从客户那里读取一个字符,增加它的值,然后再把它写回去。在更加复杂的系统中,服务器需要为每个客户执行更多的处理工作,这种一次只为一个客户服务的做法就变得不可接受了,因为其他客户只有等到服务器结束上一个客户的处理任务后才能处理它的连接。我们将在后面看到几个允许同时处理多个连接的解决方案。运行服务器程序时,它创建一个套接字并开始等待客户的连接。如果你在后台启动它,让它独立地运行,就可以在前台启动客户程序。如下所示:

$ ./server1 &
[1] 1094
$ server waiting

服务器在开始等待客户连接时会打印出一条消息。在上面的例子中,服务器等待的是一个文件系统套接字,所以可以用普通的 ls 命令来看到它。

记住:用完一个套接字后,就应该把它删除掉,即使是在程序因接收到一个信号而异常终止的情况下也应该这么做。这可以避免文件系统应充斥着无用的文件而变得混乱。

$ ls -lF server_socket
srwxr-xr-x l neil users 0 2007-06-23 11:41 server_socket= ...

访问权限前面的字母 s 和这一行末尾的等号 = 表示该设备的类型是“套接字”。套接字的创建过程与普通文件一样,它的访问权限会被当前的掩码值所修改。如果使用 ps 命令,你可以看到服务器正运行在后台。它目前处于休眠状态(STAT栏显示的是s),因此它没有消耗CPU资源。如下所示:

$ ps lx
F UID
F   UID     PID    PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0  1000  881769  881731  20   0 224340  1352 361800 R+   pts/1      0:00 ./server1

现在运行客户程序,你就可以成功地连接到服务器了。因为服务器套接字已经存在,所以你可以连接到它并与服务器进行通信。如下所示:

$ ./client1
server waiting
char from server = B

服务器的输出和客户的输出在我们的终端上混在了一起,但还是可以看出服务器从客户那里接收了一个字符,将它的值增加,然后再返回它。接着服务器继续运行并等待下一个客户的到来。如果同时运行多个客户,它们将被依次服务,但你看到的输出结果可能会更加混乱。如下所示:

$ ./client1 & ./client1 & ./client1 &
[2] 23412
[31 23413
[4] 23414
server waiting
char from server = B
server waiting
char from server = B
server waiting
char from server = B
server waiting
[2]     Done    client1
[3]-    Done    client1
[4]+    Done    client1