栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

【Linux Program】套接字

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【Linux Program】套接字

系列文章:

  • 文件操作
  • 数据管理
  • 进程和信号
  • POSIX 线程
  • 进程间通信:管道
  • 信号量共享内存和消息队列
  • 套接字

文章目录
    • 1. 什么是套接字
    • 2. 套接字连接
      • 2.1 套接字属性
      • 2.2 创建套接字
      • 2.3 套接字地址
      • 2.4 命名套接字
      • 2.5 创建套接字队列
      • 2.6 接受连接
      • 2.7 请求连接
      • 2.8 关闭套接字

之前所讨论的进程间通信的方法均是基于一台计算机系统的共享资源实现,这里的资源可以是文件系统空间、共享的物理内存或消息队列,但只有运行在同一台机器上的进程才能够使用它们。

一台机器上的进程可以使用套接字和另外一台机器上的进程通信,这样就可以支持分布在网络中的客户/服务器系统。同一台机器上的进程之间也可以使用套接字进行通信。

  • 套接字连接的工作原理
  • 套接字的属性、地址和通信
  • 网络信息和互联网守护进程
  • 客户和服务器
1. 什么是套接字

socket 是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机上进行,也可以跨网络进行,Linux 所提供的功能(如打印服务、连接数据库和提供Web页面)和网络工具(rlogin和ftp)通常都是通过套接字来进行通信的。

2. 套接字连接

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

首先,服务器应用程序用系统调用 socket 来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其它进程共享。

接下来,服务器进程会给套接字起个名字,本地套接字的名字是 Linux 文件系统中的文件名,一般放在 /tmp 或 /usr/tmp 目录中,对于网络套接字,它的名字与客户连接的特定网络有关的服务标志符(端口号或访问点),这个标识符允许Linux将进入的针对特定端口号的连接转到正确的服务器进程。

例如,Web服务器一般在80端口上创建一个套接字,这是一个专用于此目的的标识符,Web浏览器知道对于用户想要访问的Web站点,应该使用端口80来建立HTTP连接。我们用系统调用bind来给套接字命名,然后服务器进程就开始等待客户连接到这个命名套接字。系统调用 listen 的作用是,创建一个队列并将其用于存放来自客户的进入连接,服务器通过系统调用 accept 来接受客户的连接。

服务器调用 accept 时,它会创建一个与原有的命名套接字不同的新的套接字。这个新套接字只用于与这个特定的客户进行通信,而命名套接字则被保留下来继续处理来自其它客户的连接。如果服务器编写得当,它就可以充分利用多个连接带来的好处。Web服务器就会这么做以同时服务来自许多客户的页面请求。对于一个简单的服务器来说,后续的客户将在监听队列中等待,直到服务器再次准备就绪。

基于套接字系统的客户端增加简单,客户首先调用 socket 创建一个未命名套接字,然后将服务器的命名套接字作为一个地址来调用 connect 与服务器建立连接。

一旦连接建立,就可以像使用底层的文件描述符那样用套接字实现双向的数据通信。

本地客户

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int sockfd;
    int len;
    struct sockaddr_un address;
    int result;
    char ch = 'A';
    // 为客户创建一个套接字
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 根据服务器的情况给套接字命名
    address.sun_family = AF_UNIX;
    strcpy(address.sun_path, "server_socket");
    len = sizeof(address);
    // 将套接字连接到服务器的套接字
    result = connect(sockfd, (struct sockaddr *)&address, len);
    if(result == -1)
    {
        perror("oops: client1");
        exit(1);
    }
    // 通过sockfd进行读写操作
    write(sockfd, &ch, 1);
    read(sockfd, &ch, 1);
    printf("char from server = %c", ch);
    close(sockfd);
    exit(0);
}
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_un server_address;
    struct sockaddr_un client_address;
    // 删除以前的套接字,为服务器建立一个未命名的套接字
    unlink("server_socket");
    server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    // 命名套接字
    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);
    // 创建一个连接队列,开始等待客户进行连接
    listen(server_sockfd, 5);
    while (1)
    {
        char ch;
        printf("server waitingn");
        // 接受一个连接
        client_len = sizeof(client_address);
        client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_address, &client_len);
        // 对client_sockfd套接字上的客户进行读写操作
        read(client_sockfd, &ch, 1);
        ch++;
        write(client_sockfd, &ch, 1);
        close(client_sockfd);
    }
}

这个例子中的服务器程序一次只能为一个客户服务,它从客户那里读取一个字符,增加它的值,然后再把它写回去。

jiaming@jiaming-pc:~/Documents/test$ cc client1.c -o client
jiaming@jiaming-pc:~/Documents/test$ cc client1.c -o client1
jiaming@jiaming-pc:~/Documents/test$ ./client1 
oops: client1: No such file or directory
jiaming@jiaming-pc:~/Documents/tes
jiaming@jiaming-pc:~/Documents/test$ cc server1.c -o server
jiaming@jiaming-pc:~/Documents/test$ ./server &
[1] 17944
jiaming@jiaming-pc:~/Documents/test$ server waiting

jiaming@jiaming-pc:~/Documents/test$ ls -lF server_socket 
srwxrwxr-x 1 jiaming jiaming 0 5月  29 17:23 server_socket=
jiaming@jiaming-pc:~/Documents/test$ ps lx
0  1000   17944    7243  20   0   2500   576 skb_wa S    pts/1      0:00 ./server
4  1000   18051    7243  20   0  14496  3436 -      R+   pts/1      0:00 ps lx
jiaming@jiaming-pc:~/Documents/test$ cc client1.c -o client1
jiaming@jiaming-pc:~/Documents/test$ ./client1 
server waiting
char from server = Bjiaming@jiaming-pc:~/Documents/test$ 
jiaming@jiaming-pc:~/Documents/test$ ./client1 & ./client1 & ./client1 &
[2] 18297
[3] 18298
[4] 18299
jiaming@jiaming-pc:~/Documents/test$ server waiting
server waiting
char from server = Bserver waiting
char from server = Bchar from server = B
[2]   Done                    ./client1
[3]-  Done                    ./client1
[4]+  Done                    ./client1
2.1 套接字属性

套接字的特性由3个属性确定,域、类型和协议。

1. 套接字的域

域指定套接字通信中使用的网络介质,最常见的套接字域是 AF_INET,它指的是Internet网络,许多 Linux 局域网使用的都是该网络,当然因特网自身用的也是它,其底层的协议 —— 网际协议(IP)只有一个地址簇,它使用一种特定的方式来指定网络中的计算机,IP地址。

服务器计算机上可能同时有多个服务正在运行,客户可以通过 IP 端口来指定一台联网机器上的某个特定服务。在系统内部,端口通过分配一个唯一的16位的整数来标识,在系统的外部,则需要通过 IP 地址和端口号的组合来确定,套接字作为通信的终点,它必须在开始通信前绑定一个端口。

服务器在特定的端口等待客户的连接,一般情况下小于1024的端口号都是为系统服务保留的,并且所服务的进程必须具有超级用户权限。

因为标准服务都对应标准的端口号,所以计算机之间可以轻松地互连,而不需要首先协商一个正确的端口号,本地服务可以使用非标准的端口地址。

例子中的域是UNIX文件系统域AF_UNIX,即使是一台还未联网的计算机上的套接字也可以使用这个域。这个域的底层协议就是文件输入输出,而它的地址就是文件名。我们的服务套接字的地址是server_socket,当我们运行服务器程序时,就可以在当前目录下看到这个地址。

2. 套接字类型

一个套接字域可能有很多种不同的通信方式,而每种通信方式又有其不同的特性,但是 AF_UNIX 域的套接字没有这样的问题,它们提供了一个可靠的双向通信路径。在网络域中,我们需要注意底层网络的特性,以及不同的通信机制是如何受到它们的影响的。

因特网协议提供了两种通信机制:流(stream)和数据报(datagram),它们有着截然不同的服务层次。

  • 流套接字

流套接字(在某些方面类似与标准的输入/输出流)提供的一个有序、可靠、双向字节流的连接。因此,发送的数据可以确保不会丢失、复制或乱序到达,并且在这一过程中发生的错误也不会显示出来。大的消息被分片、传输、再重组。

流套接字由类型 SOCK_STREAM 指定,它们是在 AF_INET 域中通过 TCP/IP 连接实现的,它们也是AF_UNIX域中常用的套接字类型。

  • 数据报套接字

与流套接字相反,由类型 SOCK_DGRAM 指定的数据报套接字不建立和维持一个连接。它对可以发送的数据报的长度有限制,数据报作为一个单独的网络消息被传输,它可能丢失、复制或乱序到达。

数据报套接字是在 AF_INET 域中通过 UDP/IP 连接实现的,它提供一个无序的不可靠服务,但从资源的角度来看,相对来说它们开销比较小,因为不需要维持网络连接,而且因为无需花费时间来建立连接,所以它们的速度也很快。

数据报适用于信息服务中的单次查询,它主要用来提供日常状态信息或执行低优先级的日志记录,它的优点是服务器的奔溃不会给客户造成不变,也不会要求客户重启,因为基于数据报的服务器通常不保留连接信息,所以它们可以在不打扰其它客户的前提下停止并重启。

2.2 创建套接字

socket 系统调用创建一个套接字,并返回一个描述符:

#include 
#include 

int socket(int domain, int type, int protocol);

创建的套接字是一条通信线路的一个端点,domain 参数指定协议簇,type 参数指定这个套接字的通信类型,protocol 参数指定使用的协议。

domain:

AP_UNIX: UNIX 域协议,文件系统套接字
AP_INET: ARPA因特网协议,网络套接字
AP_ISO: ISO 标准协议
AP_NS: 施乐网络系统协议
AP_IPX: Novell IPX 协议
AP_APPLETALK: Appletalk DDS

type: SOCK_STREAM、SOCK_DGRAM。

SOCK_STREAM: 是一个有序、可靠、面向连接的双向字节流,对 AF_INET 域套接字来说,它默认是通过一个 TCP 连接来提供这一特性的,TCP 连接在两个流套接字端点之间建立。数据可以通过套接字连接进行双向传递。TCP 协议所提供的机制可以用于分片和重组长消息,并且可以重传可能在网络中丢失的数据。

SOCK_DGRAM 是数据报服务。可以用它来发送最大长度固定的消息,对于 AF_INET 域套接字来说,这种类型的通信是由 UDP 数据报来提供的。

通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择,只有当需要选择时,才用到 protocol参数,将该参数设置为 0 表示使用默认协议。

socket 系统调用返回一个描述符,它在许多方面类似于底层的文件描述符。当这个套接字连接到另一端的套接字后,就可以用read和write系统调用,通过这个描述符来在套接字上发送和接收数据,close系统调用用于结束套接字连接。

2.3 套接字地址

每个套接字域都有自己的地址格式,对于 AF_UNIX 域套接字来说,它的地址由结构 sockaddr_un 来描述,该结构定义在头文件 sys/un.h中。

struct sockaddr_un{
	sa_family_t sun_family; // AF_UNIX
	char sun_path[]; // pathname
}

因此,对套接字进行处理的系统调用可能需要接受不同类型的地址,每种地址格式都使用一种类似的结构来描述,它们都以一个指定地址类型(套接字域)的成员开始。

sun_path 指定的路径名长度也是有限制的,linux规定 108 个字符。

在 AF_INET 域中,套接字地址由结构 sockaddr_in 来指定,该结构定义在头文件 netinet/in.h中,至少包含以下几个成员:

struct sockaddr_in{
	short in sin_family; // AF_INET
	unsigned short int sin_port; // port number
	struct in_addr sin_addr; // internet address
};

IP 地址 in_addr 被定义为:

struct in_addr{
	unsigned long int s_addr;
};

IP 地址中的4个字节组成一个32位的值。一个 AF_INET 套接字由它的域、IP地址和端口号来完全确定。

2.4 命名套接字

要想让通过socket调用创建的套接字可以被其他进程使用,服务器程序就必须给该套接字命名。这样,AF_UNIX 套接字就会关联到一个文件系统的路径名。AF_INET 套接字就会关联到一个 IP 端口号。

#include 
int bind(int socket, const struct sockaddr *address, size_t address_len);

bind 系统调用把参数address中的地址分配给与文件描述符socket关联的未命名套接字。地址结构的长度由参数address_len传递。

地址的长度和格式取决于地址簇,bind 调用需要将一个特定的地址结构指针转换为指向通用地址类型(struct sockaddr *)。

2.5 创建套接字队列

为了能够在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求。它用 listen 系统调用来完成这一项工作。

#include 
int listen(int socket, int backlog);

linux 系统可能会对队列中可以容纳的未处理连接的最大数目做出限制。为了遵守这个最大值限制,listen 函数将队列长度设置为 backlog 参数的值。在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字。再往后的连接将被拒绝,导致客户的连接请求失败。listen 函数提供的这种机制允许当服务器正忙于处理前一个客户请求的时候,将后续的客户连接放入队列等待处理。backlog 参数常用的值是 5。

2.6 接受连接

一旦服务器程序创建并命名了套接字之后,它就可以通过 accept 系统调用来等待客户建立对该套接字的连接:

#include 
int accept(int socket, struct sockaddr *address, size_t *address_len);

accept 系统调用只有当客户程序试图连接到由 socket 参数指定的套接字上时才返回。这里的客户是指,在套接字队列中排在第一个的未处理连接。accept 函数将创建一个新套接字来与该客户进行通信,并且返回新套接字的描述符,新套接字的类型和服务器监听套接字类型是一样的。

套接字必须事先由 bind 调用命名,并且由listen调用给它分配一个连接队列。连接客户的地址将被放入address参数指向的sockaddr结构中。如果我们不关系客户的地址,也可以将address参数指定为空指针。

参数address_len指定客户结构的长度,如果客户地址的长度超过这个值,它将被截断。所以在调用 accept 之前,address_len 必须设置为预期的地址长度,当这个调用返回时,address_len 将被设置为连接客户地址结构的实际长度。

如果套接字队列中没有未处理的连接,accept 将阻塞直到有客户建立连接为止。可以通过对套接字文件描述符设置 O_NONNLOCK 标志来改变这一行为,使用的函数是 fcntl。

int flags = fcntl(socket, F_GETFL, 0);
fcntl(socket, F_SETFL, O_NONBLOCK | flags);

当有未处理的客户连接时,accept 函数将返回一个新的套接字文件描述符。发送错误时,accept 函数将返回 -1。

2.7 请求连接

客户程序通过在一个未命名套接字和服务器监听套接字之间建立的方法来连接到服务器。通过 connect 调用来完成这一工作。

#include 
int connect(int socket, const struct sockaddr *address, size_t address_len);

参数socket指定套接字将连接到参数address指定的服务器套接字,address 指向的结构的长度由参数address_len指定,参数socket指定的套接字必须是通过socket调用获得的一个有效的文件描述符。

如果连接不能立刻建立,connect 调用将阻塞一段不确定的超时时间,一旦这个超时时间到达,连接将被放弃,connect调用失败,但如果connect 调用被一个信号中断,而该信号又得到了处理,connect 调用还是会失败,但连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立。

与accept调用一样,connect 调用的阻塞特性可以通过设置该文件描述符 O_NONBLOCK 标志来改变。

2.8 关闭套接字

通过调用 close 函数来终止服务器和客户上的套接字连接,就如同对底层文件描述符进行关闭一样,你应该总是在连接的两端都关闭套接字,对于服务器来说,应该在read调用返回 0 时关闭套接字,但如果套接字是一个面向连接类型的,并且设置了 SOCK_LINCER 选项,close 选项调用会在该套接字还有未传输数据是阻塞。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/941321.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号