UNP编程:04---socket、bind、connect、listen、accept、close

 一、socket()函数

#include<sys/types.h>
#include<sys/socket.h>
int socket(int protofamily, int type, int protocol);

参数

  • 参数1:即协议域,又称为协议族(family)
    • 域参数指定通信域;这将选择用于通信的协议系列。这些族在<sys/socket.h>中定义
AF_INET用ipv4地址(32位的)与端口号(16位的)的组合
AF_INET6IPV6的

AF_LOCAL/或者AF_UNIX

(Unix域socket)用一个绝对路径名作为地址。本地通信
AF_ROUTE路由套接字
AF_IPXIPX-Novell协议
AF_PACKET低层包接口包,支持对数据链路的访问
AF_NETLINK内核用户界面设备
AF_KEY比较新,支持基于加密的安全性
  • 参数2:指定socket类型,与第三个参数有关
    • 套接字具有指定的类型,该类型指定通信语义
    • 并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。
SOCK_STREAM字节流套接字。提供序列化的、可靠的、双向连接的字节流。支持带外数据传输(TCP使用)
SOCK_DGRAM数据报套接字。(UDP使用)支持数据报(固定最大长度的无连接、不可靠的消息)
SOCK_RAW原始套接字。RAW类型,提供原始网络协议访问
SOCK_RDM提供了一个不保证排序的可靠数据报层。不过可能数据会有乱序
SOCK_PACKET专用类型。不能在通用程序中使用,它直接从设备驱动接受数据
SOCK_SEQPACKET有序分组套接字。序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
下面这两个参数是Linux 2.6.17起添加的,可以与上面的type参数进行与操作
SOCK_NONBLOCK新创建的socket设置为非阻塞的
SOCK_CLOEXECfork调用创建子进程时,在子进程中关闭该socket
  • 参数3:指定协议类型
    • 参数2的socket类型都有一个默认的协议,如果想使用默认的协议,参数3填0即可
IPPOROTO_IPIPV4网际协议
IPPOROTO_IPV6IPV6网际协议
IPPOROTO_ICMP因特网控制报文协议
IPPOROTO_RAW原始IP数据包协议
IPPROTO_TCPTCP传输协议(SOCK_STRAM默认使用)
IPPTOTO_UDPUDP传输协议(SOCK_DGRAM默认使用)
IPPROTO_SCTPSTCP传输协议
IPPROTO_TIPCTIPC传输协议

返回值

  • socket创建成功:返回一个socket描述符(sockfd),这个描述字不是固定的,为int类型
  • socket创建失败:返回-1,并设置errno变量的值

AF_XXX与PF_XXX的区别

  • 在大部分情况下,使用是相同的
  • AF_前缀表示地址族,PF_前缀表示协议族。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_值用来创建套接字,而AF_值用于套接字地址结构。但实际上,支持多个地址 族的协议族从来就未实现过,而且头文件中为一给定协议定义的PF_值总是与 此协议的AF_值相等。尽管这种相等关系并不一定永远成立,但若有人试图给已有的协议改变 这种约定,则许多现存代码都将崩溃。为与现存代码保持一致,本书中我们仅使用AF_常值, 尽管(主要是)在调用socket时我们可能会碰到PF_值

 二、connect()函数

  •  功能:客户端通过调用connect函数来建立与服务器的连接
#include<sys/types.h>
#include<sys/socket.h>
int connect(int  sockfd, const struct sockaddr  *addr, socklen_t a ddrlen);

//返回值:成功:返回0;失败:返回-1,并设置errno变量的值

参数:

  • 参数1:socket描述符(那一个套接字发出去的连接请求)
  • 参数2:要连接的套接字地址结构体
  • 参数3:对应于参数2的结构体大小

connect出错的几种情况

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

  • ①若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。举例来说,调用connect 函数时,4.4BSD内核发送一个SYN,若无响应则等待6s后再发送一个,若仍无响应则等待24s后再发送一个(TCP卷二828页)。若总共等了75s后仍未收到响应则返回本错误(有些系统提供对超时值的管理性控制,见TCP卷一的附录E)
  • ②若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上 没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误
    • RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是:目的地为某端口的 SYN到达,然而该端口上没有正在监听的服务器(如前所述);TCP想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。(TCP卷一第246~250页有更详细的信息)
  • ③若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误(soft error)。客户主机内核保存该消息,并按第一 种情况中所述的时间间隔继续发送SYN。若在某个规定的时间(4.4BSD规定75s)后仍未收到响应,则把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。以下两种情形也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;二是 connect调用根本不等待就返回

注意事项

  • ①客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口
  • ②按照TCP状态转换图,connect函数导致当前套接字从CLOSED状态(该套接字自从由socket函数创建以来一直所处的状态)转移到SYN_SENT状态,若成功则再转移到ESTABLISHED状态。若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数。在代码设计时,当循环调用函数connect为给定主机尝试各个IP地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并重新调用socket

三、bind()函数

  • 功能:把一个本地协议地址赋予给一个套接字
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd, const struct sockaddr  *addr, socklen_t  addrlen);

//返回值:成功:返回0;失败:返回-1,并设置errno变量的值

参数:

  • 参数1: socket描述符(绑定到哪一个套接字上面)
  • 参数2:指向于一个特定协议的结构体指针
  • 参数3:对应参数2的结构体长度

地址绑定的注意事项

套接字绑定的地址,地址的内容可以只指定一个端口号,或只指定一个IP地址,或者IP和端口同时指定

  • ①如果不绑定端口,则系统默认给予一个端口:
    • 服务器在启动时捆绑它们的众所周知端口。如果一个TCP客户或服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时端口。让内核来选择临时端口对于TCP客户来说是正常的,除非应用需要一个预留端口(见文章:https://blog.csdn.net/qq_41453285/article/details/102880561);然而对于TCP服务器来说却极为罕见,因为服务器是通过它们的众所周知端口被大家认识的

  • ②如果不绑定IP地址:
    • 进程可以把一个特定的IP地址捆绑到它的套接字上,不过这个IP地址必须属于其所在主 机的网络接口之一。对于TCP客户,这就为在该套接字上发送的IP数据报指派了源IP地 址。对于TCP服务器,这就限定该套接字只接收那些目的地为这个IP地址的客户连接。 TCP客户通常不把IP地址捆绑到它的套接字上。当连接套接字时,内核将根据所用外出网络 接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径(TCP卷二737页)。 如果TCP服务器没有把IP地址捆绑到它的套接字上,内核就把客户发送的SYN的目的IP 地址作为服务器的源IP地址(TCP卷二943页)

总结

  • 如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址

四、listen()函数

  • 功能:作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd, int backlog);

//返回值:成功:返回0;失败:返回-1,并设置errno变量的值

参数

  • 参数1:监听的socket描述符
  • 参数2:套接字排队的最大数目

参数2详解(未完成连接队列、已完成连接队列)

为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:

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

下图描绘了监听套接字的这两个队列:

connect的过程:

  • 每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。 连接的创建机制是完全自动的,无需服务器进程插手。下图展示了用这两个队列建立连接时所 交换的分组
  • 当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手 的第二个分节:服务器的SYN响应,其中捎带对客户SYN的ACK(2.6节)。这一项一直保留在 未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超 时为止。(源自Berkeley的实现为这些未完成连接的项设置的超时值为75s。)如果三路握手正常 完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被 投入睡眠,直到TCP在该队列中放入一项才唤醒它

关于这两个队列的处理,以下几点需要考虑:

  • ①listen函数的backlog参数曾被规定为这两个队列总和的最大值:backlog的含义从未有过正式的定义。4.2BSD的手册页面宣称它定义的是:“the maximum length the queue of pending connections may grow to”(由未处理连接构成的队列可能增长到的 最大长度)。许多手册页面甚至POSIX规范也逐字复制该定义,然而该定义并未解释未处理 连接是处于SYN_RCVD状态的连接,还是尚未由进程接受的处于ESTABLISHED状态的连接, 亦或两者皆可。这个历史性的定义出自追溯到4.2BSD版本的Berkeley的实现,后来被许多其 他实现复制。
  • ②源自Berkeley的实现给backlog增设了一个模糊因子(fudge factor):把它乘以1.5得到未处理队列最大长度(TCPv1第257页和TCPv2第462页)。举例来说,通常指定为5的backlog值实际上允许最多有8项在排队,见下图
  • ③不要把backlog定义为0,因为不同的实现对此有不同的解释(见下图)。如果你不想让任何客户连接到你的监听套接字上,那就关掉该监听套接字
  • ④在三路握手正常完成的前提下(也就是说没有丢失分节,从而没有重传),未完成连接队列中的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器。TCPv3的14.4节指出,对于一个Web服务器,许多客户与单个服务器之间的中值 RTT为187ms。(既然出现一些大值可能显著扭曲均值,对于该统计量通常使用中值。)
  • ⑤历来沿用的样例代码总是给出值为5的backlog,因为这是4.2BSD支持的最大值。这个值 在20世纪80年代是足够的,当时繁忙的服务器一天也就处理几百个连接。然而随着万维 网(World Wide Web,WWW)的发展,繁忙的服务器一天要处理几百万个连接,这个 偏小的值就根本不够了(TCPv3第187~192页)。繁忙的HTTP服务器必须指定一个大得 多的backlog值,而且较新的内核必须支持较大的backlog值(当前的许多系统允许管理员修改backlog的最大值)
  • ⑥问题是既然backlog值为5往往不够,那么应用进程应该指定多大值的backlog呢?这个问题不好回答。当今的HTTP服务器指定了一个较大的值,但是如果这个指定值在源代码中是一个常值,那么增长其大小需要重新编译服务器程序。另一个方法是设定一个默认值,不过允许通过命令行选项或环境变量覆写该默认值。指定一个比内核能够支持的值还要大的backlog也是可接受的,因为内核应该悄然把所指定的偏大值截成自身支持的最 大值,而不返回错误(TCPv2第456页)。下面给出了实际的代码。我们允许环境变量LISTENQ覆写由调用者指定的值
void Listen(int fd,int backlog)
{
    char *ptr;
    if((ptr=getenv("LISTENQ"))!=NULL)
        backlog=atoi(ptr);
    if(listen(fd,backlog)<0)
        perror("listen error");
}
  • ⑦手册和书本历来声称:将固定数目的未处理连接排成队列是为了处理服务器进程在相继的accept调用之间处于忙状态的情况。这就隐含着如此意义:在这两个队列中,已完 成队列通常应该比未完成队列有更多的项。繁忙的Web服务器再次表明这是不对的。指定较大backlog值的理由在于:随着客户SYN分节的到达,未完成连接队列中的项数可能 增长,它们等着三路握手的完成
  • ⑧当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节(TCPv2930~931页), 也就是不发送RST。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在这些队列中找到可用空间。要是服务器TCP立即响应以一个RST,客户的 connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区别响应SYN的RST究竟意味着“该端口没有服务 器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”
  • ⑨在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小

下图给出了各种操作系统下,backlog参数取不同值时已排队连接的实际数目:

  • AIX和MacOS有传统的Berkeley算法,Solaris也似乎非常接近该算法,FreeBSD则是backlog 值本身加1
  • 我们已提到过,历史上曾把backlog值指定为两个队列之和的最大值。在1996年间,因特 网受到一种称之为SYN泛滥(SYN flooding)的新型攻击[CERT 1996b]。黑客编写了一个 以高速率给受害主机发送SYN的程序,用以装填一个或多个TCP端口的未完成连接队列。(我 们用黑客(hacker)一词来指称攻击者,见[Cheswick, Bellovin, and Rubin 2003]。)而且, 该程序将每个SYN的源IP地址都置成随机数(称为IP欺骗(IP spoofing)),这样服务器的 SYN/ACK就发往不知道什么地方,同时防止受攻击服务器获悉黑客的真实IP地址。这样,通 过以伪造的SYN装填未完成连接队列,使合法的SYN排不上队,导致针对合法客户的服务被 拒绝(denial of service)。有两种处理这种拒绝服务型攻击的常用方法,[Borman 1997c]作 了总结。不过这儿我们最感兴趣的是回味一下listen的backlog参数的确切含义。它应该指定 某个给定套接字上内核为之排队的最大已完成连接数。对于已完成连接数作出限制的目的在 于:在监听某个给定套接字的应用进程(不论什么原因)停止接受连接的时候,防止内核在 该套接字上继续接受新的连接请求。如果一个系统实现了这样的解释(例如BSD/OS 3.0), 那么应用程序就无需仅仅因为服务器进程需要处理大量客户请求(例如繁忙的Web服务器) 或者为了提供对SYN泛滥的防护而指定一个巨大的backlog值了。内核处理大量的未完成连 接,而不论它们来自合法客户还是来自黑客。然而即使在这样的解释下,传统为5的backlog 值不够大的情形依然发生

五、accept函数

  • 功能:由TCP服务器调用,用于从已完成连接队列头获取一个套接字。如果队列为空,进程就被投入睡眠(假设套接字默认为阻塞方式)
#include<sys/socket.h>
#include<sys/types.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数:

  • 参数1:监听套接字描述符(哪一个套接字在接收)
  • 参数2:一个协议地址结构体,用来保存已连接的对端进程(客户)的地址
  • 参数3:对应于参数2的地址结构体大小指针

返回值:

  • 成功:返回由内核自动生成的一个全新的描述符,代表TCP客户端
  • 失败:返回-1

注意

  • 如果我们对客户端的地址不感兴趣,则参数2和参数3可以设置为空
  • ②如果没有连接请求在等待,accept会阻塞直到一个请求到来。如果sockfd处于非阻塞模式,accept会返回-1,并将errno设置为EAGAIN或EWOULDBLOCK

六、close()函数

  • 关闭相应的socket描述符
#include <unistd.h>
int close(int fd);

//返回值:成功:返回0;出错:返回-1

参数

  • 对应的socket描述符

描述符引用计数(重点)

  • 在并发服务器模型文章中末尾我们提到过,并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。既然引用计数值仍大于0,这个close调用并不引发TCP的四分组连接终止序列。 对于父进程与子进程共享已连接套接字的并发服务器来说,这正是所期望的
  • 如果我们确实想在某个TCP连接上发送一个FIN,那么可以改用shutdown函数以代替close(见文章:https://blog.csdn.net/qq_41453285/article/details/89647130
  • 我们还得清楚,如果父进程对每个由accept返回的已连接套接字都不调用close,那么并 发服务器中将会发生什么。首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻可 拥有的打开着的描述符数通常是有限制的。不过更重要的是,没有一个客户连接会被终止。当 子进程关闭已连接套接字时,它的引用计数值将由2递减为1且保持为1,因为父进程永不关闭任何已连接套接字。这将妨碍TCP连接终止序列的发生,导致连接一直打开着
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

董哥的黑板报

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值