Socket03-:套接字API

Socket 03-:套接字API


在这里插入图片描述

1. 套接字的创建和关闭:套接字描述符

套接字是 Unix 基本文件类型的一种,可以使用文件 I/O 的大部分函数,fchdir(2) 只适用于目录文件,而以下是否可以与实现有关,通常不允许使用:fchmod(2), ftruncate(2), lseek(2), mmap(2);

在这里插入图片描述

1.1 socket函数:用于创建并打开一个套接字

socket 函数在成功时返回一个小的非负整数值,它与文件描述符类似,我们把它称为套接字述符(socket descriptor),简称 sockfd。为了得到这个套接字描述符,我们只是指定了议族( IPv4、IPv6 或 Unix)和套接字类型(字节流、数据报或原始套接字)。我们并没有指定本地协议地址或远程协议地址。

#include <sys/socket.h> 
int socket( int family, int type, int protocol); 
        返回:若成功则为非负描述符,若出错则为 -1
  • family参数:指明协议族,该参数被称为协议域
family说明
AF_INETIPv4协议
AF_INET6IPv6协议
AF_LOCALUnix域协议
AF_ROUTE路由套接字
AF_KEY密钥套接字
  • type参数:为套接字类型选项,主要包括
type说明
SOCK_STREAM字节流套接字
SOCK_DGRAM数据报套接字
SOCK_SEQPACKET有序分支套接字
SOCK_RWA原始套接字
  • protocol参数:为协议选项,为 0 时使用 type 的默认值。可以通过 getprotoent(3)转换协议名为协议值。
protocol说明
IPPROTE_TCPTCP传输协议
IPPROTE_UCPUDP传输协议
IPPROTE_SCTPSCTP传输协议

socket函数中family和type参数的组合:

在这里插入图片描述

1.2 close(2)关闭套接字、shutdown(2)关闭套接字的一端、SO_LINGER 套接字选项对于关闭套接字的影响

套接字通常使用标准的 close 函数关闭,不过我们将看到使用 shutdown 函数关闭套接字的一端,我们还要查看 SO_LINGER 套接字选项对于关闭套接字的影响。

1.2.1 close函数:关闭套接字
#include <unistd.h> 
int close( int sockfd); 
        返回:若成功则为 0,若出错则为 -1

close 一个 TCP 套接字的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。

每个描述符会有一个对应的描述符引用计数,调用 close 会将引用计数减 1,当引用计数为 0时,才会引发 TCP 的四个分组连续终止序列。

注意:在并发服务器中父进程 fork 子进程后会调用close 关闭已连接套接字,如果父进程对每个由 accept 返回的已连接套接字都不调用 close,那么父进程最终将耗尽可用描述符。

1.2.2 shutdown(2)关闭套接字的一端

能够用close关闭套接字,还使用shutdown的理由:

  • (1) close 把描述符的引用计数减 1,仅在该计数变为 0 时才关闭套接字。使用 shutdown 可以不管引用计数就激发 TCP 的正常连接终止序列。。
  • (2) 有时只关闭套接字双向传输中的一个方向会很方便。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
                返回值:若成功,返回 0;若出错,返回 -1
  • howto 包括 SHUT_RD(关闭读端,则无法从套接字读取数据)、SHUT_WR、SHUT_RDWR;

  • SHUT_RD 关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个 TCP 套接字这样调用shutdown 函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃

  • SHUT_WR 关闭连接的写这一半——对于 TCP 套接字,这称为半关闭(half-close)。当前 留在套接字发送缓冲区中的数据将被发送掉,后跟 TCP 的正常连接终止序列。不管套接字描述符的引用计数是否等于 0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。

  • SHUT_RDWR 连接的读半部和写半部都关闭

1.2.3 SO_LINGER 套接字选项对于关闭套接字的影响

2. connect函数:建立连接

面向流的协议如 TCP(7),需要建立连接才能进行数据传输。在 C/S 模型中,建立连接的请求通常为客户机向服务器提出。请求建立连接的函数为:

#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
                返回值:若成功,返回 0;若出错,返回 -1
  • 该函数请求套接字 sockfd (由socket函数返回的套接字描述符)连接到地址标识 servaddr (servaddr为套接字地址结构的指针,addrlen为该结构的大小)端对应的套接字。

如果是 TCP 套接字,调用 connect 函数将激发 TCP 的三路握手过程,而且在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况。

  • (1) 若 TCP 客户没有收到 SYN 分节的响应,则返回 ETIMEDOUT 错误。举例来说,调用connect 函数时,4.4BSD 内核发送一个 SYN,若无响应则等待 6s 后再发送一个,若仍无 响应则等待 24s 后再发送一个。若总共等了 75s 后仍未收到响应则返回本错误。
  • (2) 若对客户的 SYN 的响应是 RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接( 例如服务器进程也许没在运行)。这是一种硬错误( hard error)客户一接收到 RST 就马上返回 ECONNREFUSED 错误
    • RST 是 TCP 在发生错误时发送的一种 TCP 分节。产生 RST 的三个条件是:目地 为某端口的 SYN 到达,然而该端口上没有正在监听的服务器( 如前所述);TCP想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。
  • (3) 若客户发出的 SYN 在中间的某个路由器上引发了一个“ destination unreachable”( 目的地不可达)ICMP 错误,则认为是一种软错误(soft error)。客户主机内核保存该消息,并按第一种情况中所述的时间间隔继续发送 SYN。若在某个规定的时间( 4.4BSD 规定 75s)后仍未收到响应,则把保存的消息( 即 ICMP 错误)作为 EHOSTUNREACH 或 ENETUNREACH 错误返回给进程。

connect 函数导致当前套接字从 CLOSED 状态( 该套接字自从由 socket 函数创建以来一直 所处的状态)转移到 SYN_ SENT 状态,若成功则再转移到 ESTABLISHED 状态。若connect 失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用 connect 函数。客户接收到三路握手的第二个分节时,connect 返回,而服务器要直到接收到三路握手的第三个分节才返回,即在 connect 返回之后再过一半 RTT 才返回

**注意:**connect 和 accept 都是低速系统调用,即它们在资源不可用下将永远阻塞直到被一个信号打断;可以用 ioctl(2)或者 fcntl(2)设置对应的套接字描述符为非阻塞模式,或者使用有多路转接与等待超时功能的 select(2)和 poll(2),描述符可读表示有连接请求在等待处理,描述符可写表示连接建立成功。

3. bind函数:将套接字绑定到本地协议地址

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
                返回值:若成功,返回 0;若出错,返回 -1
  • 对 myaddr 的成员有如下限制:
    • 必须使用本地地址(在进程正在运行的计算机上,指定的地址必须有效;不能指定一个其他机器的地址)
    • 必须与 socket(2)创建时的 family 格式匹配;
    • 只有 root 进程的端口号可以小于 1024;
  • 调用 bind 可以指定 IP 地址或端口,可以两者都指定,也可以都不指定。
    • 如果一个 TCP 客户或服务器未曾调用 bind 捆绑一个端口,当调用 connect 或 listen 时,内核就要为相应的套接字选择一个临时端口。
    • TCP 客户通常不把 IP 地址捆绑到它的套接字上,当连接套接字时,内核将根据所用出网络接口来选择源 IP 地址,而所用外出接口则取决于到达服务器所需的路径。如果 TCP 服务器没有把 IP 地址捆绑到它的套接字上,内核就把客户发送的 SYN 的目的IP 地址作为服务器的源 IP 地址(捆绑通配地址是在告知系统:要是系统是多宿主机,我们将接受目的地址为任何本地接口的连接 )。

在这里插入图片描述

  • 对于 IPv4 来说,通配地址由常值 INADDR_ANY 来指定,其值一般为 0。对于 IPv6来书,系统预先分配 in6addr_any 变量并将其初始化为常值 IN6ADDR_ANY_INIT。头文件< netinet/in.h> 中含有 in6addr_any 的 extern 声明。
IPv4通配地址:
struct sockaddr_in   servaddr; 
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* wildcard */

IPv6通配地址:
struct sockaddr_in6  serv; 
serv.sin6_addr = in6addr_any; /* wildcard */

如果让内核来为套接字选择一个临时端口号,那么必须注意,函数 bind 并不返回所选择的值。为了得到内核所选择的这个临时端口值,必须调用函数 getsockname 来返回协议地址。

4. listen函数

listen 函数仅由 TCP 服务器调用,它做两件事情。

  • (1) 当 socket 函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用 connect 发起连接的客户套接字。listen 函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用 listen 导致套接字 CLOSED 状态转换到 LISTEN 状态
  • (2) 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。
#include < sys/socket.h> 
int listen( int sockfd, int backlog); 
        返回:若成功则为 0,若出错则为- 1

内核为任何一个给定的监听套接字维护两个队列:

  • (1) 未完成连接队列(incomplete connection queue),每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三路握手程。这些套接字处于 SYN_RCVD 状态
  • (2) 已完成连接队列(completed connection queue),每个已完成 TCP 三路握手程的客户对应其中一项。这些套接字处于 ESTABLISHED 状态

在这里插入图片描述

在这里插入图片描述

当来自客户的 SYN 到达时,TCP 在未完成连接队列中创建一个新项,然后响应以三路握手的二个分节:服务器的 SYN 响应,其中捎带对客户 SYN 的 ACK。这一项一直保留在未完成连接列 中,直到三路握手的第三个分节(客户对服务器 SYN 的 ACK)到达或者该项超时为止( 源自 Berkeley 的实现为这些未完成连接的项设置的超时值为 75s)。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用 accept 时,已完成连接队列中的 队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到 TCP 在该队列中放入一项才唤醒它。

5. accept函数

accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。

#include <sys/socket.h> 
int accept( int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);         
        返回:若成功则为非负描述符,若出错则为 -1
  • sockfd参数是一个已经创建的套接字描述符,并且使用bind函数将其绑定到了端口上,并且正在使用listen函数侦听端口
  • 参数 cliaddr 和 addrlen 用来返回已连接的对端进程(客户)的协议地址。addrlen 是值—结果参数:调用前,我们将由 *addrlen 所引用的整数值置为由 cliaddr 所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。
  • 如果 accept 成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP 连接。在讨论 accept 函数时,我们称它的第一个参数为监听套接字(listening socket)描述符(由 socket 创建,随后用作 bind 和 listen 的第一个参数的描述符),称它的返回值为已连接套接字(connected socket)描述符
    • 一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的 TCP 三路握手过程已经成)。 当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。

6. getsockname 和 getpeername 函数

#include <sys/socket.h> 
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen); 
        均返回:若成功则为 0,若出错则为 -1
  • 注意,这两个函数的最后一个参数都是值-结果参数。
    • 如果地址和缓冲区长度不匹配,地址会被自动截断而不报错。
    • 如果当前没有地址绑定到该套接字,则其结果是未定义的。

两函数的使用情景:

  • TCP客户端没有使用 bind (或者 TCP 服务器使用通配 IP 地址调用 bind 时), getsockname 用于返回由内核赋予该连接的本地 IP 地址和本地端口号。
  • 当一个服务器是由调用过 accept 的某个进程通过调用 exec 执行程序时,它能够获取客户身份的唯一途径便是调用 getpeername。

inetd fork 并 exec 某个 TCP 服务器程序。inetd 调用 accept(左上方方框)返回两个值:已连接套接字描述符 connfd,这是函数的返回值;客户的 IP 地址及端口号,如图中标有“对端地址” 的小方框所示(代表一个网际网套接字地址结构)。inetd 随后调用 fork,派生出 inetd 的一个子进程。既然子进程起始于父进程的内存映像的一个副本,父进程中的那个套接字地址结构在子进程中也可用,那个已连接套接字描述符也是如此(因为描述符在父子进程之间是共享的)。然而当子进程调用 exec 执行真正的服务器程序(譬如说 Telnet 服务器序)时,子进程的内存映像被替换成新的 Telnet 服务器的程序文件(也就是说包含对端地址的那个套接字地址结构就此丢失),不过那个已连接套接字描述符跨 exec 继续保持开放。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值