linux c套接字编程(2) - socket介绍

2018-06-23 18:40:54

socket 是一种 IPC 方法,它允许位于同一主机或使用网络连接起来不同主机上的应用程序之间交换数据。socket io 可以使用传统的read() 和 write()系统调用或一组 socket 特有的系统调用如 send()、recv()、sendto()以及recvfrom()来完成。默认是阻塞的,可以通过使用fcntl() 设置 O_NONBLOCK。

创建一个 socket:socket()

#include <sys/socket.h>

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

//Returns file descriptor on success, or –1 on error

type 参数指定 socket 类型。

 type描述 
 SOCK_STREAM 流
 SOCK_DGRAM 数据报
 SOCK_NONBLOCK IO操作非阻塞
 SOCK_CLOEXEC close-on-exec

domain参数如下表。

 domain 执行的通信 通信范围 地址格式 地址结构
 AF_UNIX 内核 同一主机 路径名 sockaddr_un
 AF_INET IPv4 IPv4网络 32位IPv4+16位端口 sockaddr_in
 AF_INET6 IPv6 IPv6网络 128位IPv6+16位端口 sockaddr_in6


将socket绑定到地址:bind()

bind() 系统调用将一个socket绑定到一个地址上。

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//Returns 0 on success, or –1 on error

sockfd 参数是在上一个 socket() 调用中获得的文件描述符。addr 参数是一个指针,它指向一个指定该 socket 绑定到的地址的结构。addrlen 指定了地址结构的大小。


通用socket地址结构:struct sockaddr

传入 bind() 的 addr 和 addrlen 参数比较复杂,有必要对其做进一步解释。从上表中可以看出每种 socket domain 都使用了不同的地址格式。如 UNIX domain socket 使用路径名,而 Intemet domain socket 使用 IP 地址和端口号。对于各种 socket domain 都需要定义一个不同的结构类型来存储 socket 地址。然而由于诸如 bind() 之类的系统调用适用于所有 socket domain , 因此它们必须要能够接受任意类型的地址结构。为支持这种行为, socket API 定义了一个通用的地址结构 struct sockaddr 。这个类型的唯一用途是将各种 domain 特定的地址结构转换成单个类型以供 socket 系统调用中的各个参数使用。 sockaddr 结构通常被定义成如下所示的结构。

struct sockaddr {
    sa_family_t sa_family;   /* Address family (AF_* constant) */
    char sa_data[14];     /* Socket address (size varies
                           according to socket domain) */
};


流 socket

流socket通常可以分为主动和被动两种,默认为主动。大多数应用程序中,服务器会执行被动式打开,而客户端会执行主动式打开。在后面的小节中将会假设这种场景。


监听接入链接:listen()

listen() 系统调用将文件描述符 sockfd 引用的流 socket 标记为被动。这个socket 会被后面其他socket主动连接。

#include <sys/socket.h>

int listen(int sockfd, int backlog);

//Returns 0 on success, or –1 on error

backlog 参数用来指定排队的主动连接数(服务端来不及处理的连接),SOMAXCONN 常量指定了等待连接的上限值,在linux上定义为128,linux上允许在运行时通过 /proc/sys/net/core/somaxconn 文件来调整这个限制。


接受连接:accept()

在文件描述符 sockfd 引用的监听流 socket 上接受一个接入链接。如果在调用 accept() 时不存在一个 等待被接受(pending)的连接,那么会阻塞。

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

//Returns file descriptor on success, or –1 on error

理解 accept() 的关键点是它会创建一个新 socket ,并且正是这个新 socket 会与执行 connect() 的对等 socket 进行连接。

accept() 调用返回的函数结果是已连接的 socket 的文件描述符。传入 accept() 的剩余参数会返回对端 socket 的地址。 addr 参数指向了一个用来返回 socket 地址的结构。这个参数的类型取决socket domain。 addrlen 参数是一个值一结果参数。它指向一个整数,在调用被执行之前必须要将这个整数初始化为 addr 指向的缓冲区的大小,这样内核就知道有多少空间可用于返回 socket 地址了。当 accept() 返回之后,这个参数会被设置成实际被复制进缓冲区中的数据的字节数。如果不关心对等 socket 的地址,那么可以将 addr 和 addrlen 分别指定为 NULL 和 0 。(如果希望的话可以像后面描述那样在后面某个时刻使用 getpeername() 系统调用来获取对端的地址。)


连接到对等 socket:connect()

connect() 将主动socket的文件描述符 sockfd 连接到通过 addr 和 addrlen指定的监听 socket 上。

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//Returns 0 on success, or –1 on error

如果连接失败,那么为了可移植性考虑,先关闭 socket,创建新的 socket ,在该 socket 上重新进行连接。


流 socket I/O

连接流 socket I/O 的语义与管道 I/O 的语义类似。

1.要执行 I/O 需要使用 read() 和 write() 系统调用(或 socket 特有的 send() 和 recv() 调用)。由于 socket 是双向的,因此在连接的两端都可以使用这两个调用。

2.一个 socket 可以使用 close() 系统调用来关闭或在应用程序终止之后关闭。之后当对等应用程序试图从连接的另一端读取数据时将会收到文件结束(当所有缓冲数据都被读取之后)。如果对等应用程序试图向其 socket 写入数据,那么它就会收到一个 SIGPIPE 信号,并且系统调用会返回 EPIPE 错误。处理这种情况的常见方式是忽略 SIGPIPE 信号并通过 EPIPE 错误找出被关闭的连接。


连接终止:close()

终止一个流 socket 连接的常见方式是调用 close()。如果多个文件描述符引用了同一个 socket ,那么当所有描述符被关闭之后连接就会终止。

假设在关闭一个连接之后,对等应用程序崩溃或没有读取或错误处理了之前发送给它的数据。在这种情况下就无法知道己经发生了一个错误。如果需要确保数据被成功地读取和处理,那么就必须要在应用程序中构建某种确认协议。这通常由一个从对等应用程序传过来的显式的确认消息构成。在后面将会描述 shutdown() 系统调用,它为如何关闭一个流 socket 连接提供了更加精细的控制。


数据报 socket

recvfrom() 和 sendto() 系统调用在一个数据报 socket 上接收和发送数据报。

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buffer, size_t length, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);

//Returns number of bytes received, 0 on EOF, or –1 on error

ssize_t sendto(int sockfd, const void *buffer, size_t length, int flags,
                const struct sockaddr *dest_addr, socklen_t addrlen);

//Returns number of bytes sent, or –1 on error

这两个系统调用的返回值和前三个参数与 read() 和write() 中的返回值和相应参数是一样的。

第四个参数flags是一个位掩码,它控制着了 socket特定的IO特性。在61.3节中介绍 recv()和send()系统调用时将对这些特性进行介绍。如果无需使用其中任何一种特性,那么可以将fags 指定为0。

src_addr 和 addrlen参数被用来获取或指定与之通信的对等 socket的地址。 

对于 recvfrom 来讲, src_addr 和 addrlen参数会返回用来发送数据报的远程 socket的地址。 src_addr参数是一个指针,它指向了一个与通信 domain匹配的地址结构。与 accept() 一样 addrlen是一个值-结果参数。在调用之前应该将 addrlen初始化为 src_addr指向的结构的大小; 在返回之后,它包含了实际写入这个结构的字节数。 

如果不关心发送者的地址,那么可以将 src_addr 和 addrlen都指定为NULL。在这种情况下, recvfrom() 等价于使用 recv()来接收一个数据报。也可以使用 read()来读取一个数据报,这等价于在使用 recv()时将flags参数指定为0。

不管 length的参数值是什么, recvfrom() 只会从一个数据报 socket中读取一条消息。如果 消息的大小超过了 length字节,那么消息会被截断为 length字节。

对于 sendto() 来讲, dest_addr 和 addrlen参数指定了数据报发送到的 socket。这些参数的 使用方式与 connect() 中相应参数的使用方式是一样的。 dest_addr参数是一个与通信 domain匹配的地址结构,它会被初始化成目标 socket的地址。 addrlen参数指定了addr的大小。在 Linux上可以使用 sendto 发送长度为0的数据报,但不是所有的UNIX实现都允许这样做的。

在数据报 socket 上使用 connect()

尽管数据报 socket是无连接的,但在数据报 socket上应用 connect() 系统调用仍然是起作用的。在数据报 socket上调用 connect() 会导致内核记录这个 socket的对等 socket的地址。术语已连接的数据报 socket就是指此种 socket。术语非连接的数据报 socket是指那些没有调用 connect() 的数据报 socket(即新数据报 socket的默认行为)。 当一个数据报 socket已连接之后: 

1.数据报的发送可在 socket上使用 write(或 send())来完成并且会自动被发送到同样的对等 socket上。与 sendto() 一样,每个 write调用会发送一个独立的数据报。

2.在这个 socket上只能读取由对等 socket 发送的数据报。

 注意 connect() 的作用对数据报 socket是不对称的。上面的论断只适用于调用了 connect() 数据报 socket,并不适用于它连接的远程 socket(除非对等应用程序在其 socket上也调用了 connect())。

通过再发起一个 connect() 调用可以修改一个已连接的数据报 socket的对等 socket。此外, 通过指定一个地址族(如 UNIX domain中的 sun_family字段)为 AF_UNSPEC的地址结构还可以解除对等关联关系。但需要注意的是,其他很多UNIX实现并不支持将 AF_UNSPEC用 于这种用途。

为一个数据报 socket设置一个对等 socket,这种做法的一个明显优势是在该 socket上传输数据时可以使用更简单的I/O系统调用,即无需使用指定了 dest_addr 和 addrlen参数的sendto(),而只需要使用 write()即可。设置一个对等 socket主要对那些需要向单个对等 socket (通常是某种数据报客户端)发送多个数据报的应用程序是比较有用的 在一些TCP/P实践中,将一个数据报 socket连接到一个对等 socket能够带来性能上的 提升。在 Linux上,连接一个数据报 socket 对性能影响较少。


©著作权归作者所有
收藏
推荐阅读
  • err
    linux c套接字编程(1) - TCP/IP网络基础

    一个联网协议是定义如何在一个网络上传输信息的一组规则。联网协议通常会被组织成一系列的层,其中每一层都构建于下层之上并提供特性以供上层使用。TCP/IP 协议套件是一个分层...

  • err
    linux c文件锁 flock()、fcntl()

    同步技术我们有讲过信号,本篇我们介绍专门为文件设计的同步技术。由于 stdio 库会在用户空间进行缓冲,因此在混合使用 stdio 函数与本章介绍的加锁技术时需要特别小心...

  • linux c虚拟内存操作-mprotect()、mlock()、mlockatt()

    改变内存保护:mprotect()mprotect()系统调用修改起始位置为addr,长度为length字节的虚拟内存区域中分页上的保护。addr取值必须为分页大小的整数倍,length会被向上舍入到...

  • err
    linux c内存映射

    内存映射一般是用来做ipc进程间通信的,类似于一种共享内存技术。内存映射分为4种(假设有进程A和进程B):1.私有文件映射:2个进程都以一个文件中的内容来初始化内存内容,...

  • err
    linux c管道和FIFO

    每个 shell 用户都对命令中使用管道比价熟悉,如下面这个统计一个目录中文件的数目的命令所示。ls | wc -l为了执行上面的命令,shell 创建了2个进程分别执行...

  • nginx模块 ngx_http_headers_module

    ngx_http_headers_module 模块是用来增加 Expires 和 Cache-control,或者是任意的响应头。Syntax: add_header name value [alw...

  • nginx模块 ngx_http_gunzip_module、ngx_http_gzip_module、ngx_http_gzip_static_module

    ngx_http_gunzip_module 模块将文件解压缩后并在响应头加上 "Content-Encoding: gzip" 返回给客户端。为了解决客户端不支持gzip压缩。编译的时候带上 --w...

  • nginx模块 ngx_http_flv_module、ngx_http_mp4_module

    ngx_http_flv_module模块提供了对 flv 视频的伪流支持。编译的时候带上 --with-http_flv_module。它会根据指定的 start 参数来指定跳过多少字节,并在返回数...

  • nginx模块 ngx_http_fastcgi_module

    ngx_http_fastcgi_module 模块使得nginx可以与 fastcgi 服务器通信。比如目前要使得 nginx 支持 php 就得使用 fastcgi技术,在服务器上装上 nginx...

  • nginx模块 ngx_http_autoindex_module

    ngx_http_autoindex_module 模块可以将uri以 / 结尾时,列出里面的文件和目录。Syntax: autoindex on | off; Default: autoindex ...

简介
天降大任于斯人也,必先苦其心志。