现在的位置: 首页 > 综合 > 正文

UNIX网络编程—基本TCP套接字编程(四)

2013年12月06日 ⁄ 综合 ⁄ 共 6171字 ⁄ 字号 评论关闭
文章目录

从本章开始客户/服务器之间通信程序的编写

一、基本的socket函数

执行网络I/O,进程必须调用socket函数,指定期望的通信协议类型(TCP、UDP、UNIX域讨套接字等)。

#include <sys/socket.h>
int socket(int family,int type,intprotocol);
//若成功则为非负的描述符,失败则为-1

family参数指定协议簇,该参数也称为协议域。Type参数指明套接字类型。Protocol参数为某个协议类型参数,后者设为0.以选择所给定family和type组合的系统默认值。

从这里开始,整个客户/服务器程序的编写流程均为这个

Family

说明

AF_INET

Ipv4

AF_INET6

Ipv6协议

AF_LOCAL

UNIX域协议

AF_ROUTE

路由器套接字

AF_KEY

密钥套接字

Socket函数中family常值

Type

说明

SOCK_STREAM

字节流套接字

SOCK_DGRAM

数据报套接字

SOCK_SEQPACKET

有序分组套接字

SOCK_RAW

原始套接字

Socket函数综合功能type中的常值

Protocol

说明

IPPROTO_CP

TCP传输协议

IPPROTO_UDP

Udp传输协议

IPPROTO_SCTP

SCTP传输协议

Socket函数AF_INET或者AF_INET6的protocol常值

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

对比AF_XXX和PF_XXX

AF_前缀标识地址簇,PF_前缀表示协议簇。这两者的区别不是很重要,在很多套接字上的编程都是使用AF_,只要直到这个类型就可以。

二、connect函数

  TCP客户用connect函数来建立与TCP服务器的连接。

#include <sys/socket.h>

int connect(int sockfd,const structsockaddr  * servaddr,socklen_t addrlen); //若成功则为0,失败则为-1

      sockfd是由socket函数返回的套接字描述符,第二个、第三个参数分别是一个指向套接字地址结构的指针和该结构的大小,套接字地址结构必须含有服务器的IP地址和端口号。客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。(因为客户端使用此函数,客户端绑定也没什么用途)

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

        若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。距离来说,调用connect函数时,内核发送一个SYN,若无响应则等待6s后再发送一个,若扔无响应则等待24s再发送一个,若总共等了75s后仍无收到响应则返回本错误。

        若对客户的SYN响应为RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误,客户一接收到RST就马上返回ECONNREFUSED错误。

        RST是TCP在发生错误时发送的一种TCP分节。产生RST的三个条件是,:目的地位某端口的SYN到达,然而该端口上没有正在监听的服务器;TCP想去想一个已有链接;TCP接受到一个根本不存在的链接上的分节。

        若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错误。客户主机内核保存该消息,并按第一种情况中所述的时间间隔继续发送SYN。若在某个规定的时间后仍未收到响应,则把保存的消息作为RHOSTUNREACH或ENETUNREACH错误返回给进程。

        若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数,当循环调用函数connect为给定主机尝试各个ip地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并从新调用socket.

三.bind函数

bind函数把一个本地协议地址赋予一个套接字,对于网际网协议,协议地址是32位的Ipv4地址或者IPV6地址与16位的TCP或UDP端口号的组合。

#include <sys/socket.h>

Int bind(int sockfd,const struct sockaddr*myaddr,socklen_t addrlen);//如成功则为0,失败则为-1

        第二个参数是一个指向特定于协议的地址结构的指针,第三个参数是该地址结构的长度。对于TCP,调用bind函数可以主动一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定。

服务器在启动时捆绑他们的周所周知端口,如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或者listen时,内核就要为响应的套接字选择一个临时端口。让内核俩选择临时端口对于TCP客户来说是正常的,除非应用需要一个预留端口,然而对于TCP服务器来说却极为罕见,因为服务器是通过他们的众所周知端口被大家认识的。

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

如果让内核啦为套接字选择一个临时端口号,函数bind并不返回所选择的值。由于bind函数的第二个参数有const限定词,它无法返回所选之值。为了得到内核所选择的这个临时端口,必须调用函数getsockname来返回协议地址。

进程捆绑非统配IP地址到套接字上的常见例子是在为多个组织童工WEB服务器的主机上。首先,每个组织都得有各自的域名,譬如这样的形式:www.organization.com。其次,每个组织的域名都映射到不同的IP地址,不过通常仍在同一子网上。举例说明,如果子网是192.168.10,那么第一个组织的IP地址可以是192.168.10.128,第二个组织的可以是192.168.10.129,等等。然后把所有这些IP地址都定义成单个网络接口的别名。(有的系统就使用ifconfig命令的alisas选项来定义),这么一来,IP层将接受所有目的地为任何一个别名地址的外来数据报。最后,为灭个祖师启动一个HTTP服务器的副本,每个副本仅仅绑定相应组织的IP地址。

五、listen函数

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

1)  当socket函数创建一个套接字时,他被假设为一个中东套接字,也就是说,它是一个将调用connect发起连接的客户套接字。Listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应该接受指向该套接字的链接请求,

#include <sys/socket.h>

lnt listen(int sockfd,int backlog);// 若成功则为0,失败则为-1

应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。

2)  函数总的第二个参数设置了相应套接字最大连接数

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

为完成链接队列,每个这样的SYN分节对应其中一项,已由某个客户发送并到达服务器,而服务器正在等待完成相应的TCP三路握手,这些套接字处于SYN_RECVD状态

已完成链接队列,每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态,也就是数据传送状态。

每当在未完成链接队列中创建一项时,来自监听套接字的参数就复制到即将建立的链接中。链接的创建机制是完全自动的,无需服务器进程查收。下图是这两个队列链接是所交换的分组

        当来自客户的SYN到达时,TCP在未完成链接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中捎带对客户的SYN的ACK。这一项一直保留在未完成链接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。如果三路握手正常完成,该项就从未完成链接队列中的对头项将返回给进程,或者吐过该队列为空,那么进程将被投入睡眠,直到TCP在该队列中国放入一项才唤醒它。在三路握手正常完成的前提下(没有丢失分节,从而没有重传),未完成链接队列中的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器。对于一个WEB服务器,许多客户与单个服务器之间的中值RTT为187ms。

       关于这两个队列需要特别注意的一点:当一个客户SYN到达时,拖这些队列是满的,TCP就忽略该分节,也就是不发送RST。这么做事因为:这种情况是暂时的,客户TCP将重发SYN,期望不久久能在这些队列中找到可用空间。要是服务器TCP立即响应以一个RST。客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区别响应SYN的RST究竟意味着“该端口乜有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”。

        在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接受缓冲区大小。

六、accept函数

Accept函数由TCP服务器调用,用于从已完成链接队列对头返回下一个已完成俩接,如果已完成链接队列为空,那么进程被投入睡眠。

#include <sys/socket.h>

Int accept(int sockfd,struct sockaddr *cliaddr,socklen_t*addrlen); // 若成功为非负描述符,若出错则为-1

        参数.cliaddr和addrlen用来返回已链接的对端进程(客户)的协议地址。addrlen是指-结果参数。调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度。返回时,该整数值既为由内核存放在该套接字地址结构内的确切字节数。

如果成功,返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP链接。

七、fork和exec函数

#include <unistd.h>

Pid_t fork(void); //在子进程中为0,在父进程中为子进程ID,出错为-1

          在子进程返回值为0,在父进程返回值为子进程ID,出错为-1.fork在子进程返回0而不是父进程ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的进程ID。相反,父进程有许多子进程,而且无法获取各个子进程的进程ID。

1)  关于exec函数

关于exec函数有六个,这六个exec函数之间的区别在于:待执行的程序文件是由文件名还是由路径名指定;新程序的参数是一一列出还是由一个指针数组来引用;把调用进程的环境传递给新程序还是给新程序指定新的环境。

八、并发服务器

        当一个连接建立,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字connfd),父进程则等待另一个连接(通过监听套接字listenfd)。既然新的客户由子进程童工服务,父进程就关闭已连接套接字。子进程在做完操作以后,显示地关闭已连接套接字。这一点并非必须,因为下一个语句就是调用exit,而进程终止处理的部分工作就是关闭所所有由内核打开的描述符。

        同时这里还牵涉到引用计数的问题,在父进程中可以关闭了连接套接字,为何在子进程中仍然可以使用呢?引用计数是当前打开着的引用该文件或套接字的描述符个数。比如socket返回后和listenfd关联的文件表项的引用计数值为1.accept返回后与connfd关联的文件表项的引用计数为1.进过fork以后,这两个描述符就在父进程与子进程之间共享,因此与这两个套接字相关联的文件表项各自的访问计数均为2.当父进程关闭connfd时,它只是把相应的引用计数值从2减为1.该套接字真正的清理和资源释放在其引用计数达到0时才发生。

        在接下来的章节中将展示并发服务器。

for(;;)
	{
		connfd=accept(listenfd,(struct sockaddr*)&cliaddr,&connlen);
		if((pid_t=fork())==0)
			{
				close(listenfd);
				str_echo(connfd);
				exit(0);
				}
		close(connfd);
		}

这就是并发服务器和迭代服务器最重要的区别,这里仅仅是核心部分代码

        最典型的步骤就是:在父进程中fork一个子进程,然后在父进程中close连接描述符,在子进程中close监听描述符,关于为何这样做,在下文中有介绍,主要的核心就是描述符计数值的问题。

九、close函数

#include  <unistd.h>

Int close(int sockfd);

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

如果父进程对每个由accept返回的已链接套接字不调用close。那么,父进程最终将耗尽可用描述符,更重要的是,没有一个客户连接会被终止。当子进程关闭已链接套接字时,它的应用技术值将由2减为1并保持不变。这将妨碍TCP链接终止序列的繁盛,导致链接一直打开着。

十、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

       函数的作用:

1)  在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。

2)  当一个服务器调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径是调用getpeername。

PS:

   本章主要讲解了TCP编程初步知识,特别是几个重要函数的使用

抱歉!评论已关闭.