MP77的UNIX课件笔记(12)

本章介绍socket网络编程,socket是当前计算机网络中最流行的编程接口,同时也是UNIX系统应用中必不可少的关键模块之一。我们会介绍计算机网络的基本概念和原理,但仍然需要读者具备一定的计算机网络通信基础知识。在后继连载中,会经常引用本文讨论的内容。

1 socket网络编程概述

1.1 socket概述

网络协议规定了两台计算机之间进行数据交换的共同规则,包括交换数据的格式和动作序列。但并不规定在一台计算机内利用网络协议传输信息的应用程序和实现这些协议的协议实体之间的程序接口。

UNIX中传输层和传输层以下的协议在操作系统内核中实现,那么,就必须规定一种应用程序使用内核的这些网络功能的方法。UNIX总是习惯于将设备和其他机制组织成文件的方式,然后通过文件描述符像访问普通文件那样访问它们。UNIX访问网络也用文件描述符引用一个特殊文件的方法。网络机制要比终端和管道复杂得多,所以,还提供了一组施加在这种特殊文件描述符上的系统调用实现网络所必需的功能。这组函数,就是应用程序和网络之间的接口。

应用程序与网络之间接口有socket和TLI。socket最先由BSD 4.3提供,TLI(tansport layer interface)由AT&T的System V提供。除此之外,还有其他的几种接口,但没有被程序员普遍接受。

对于socket和TLI,多数程序员更偏爱socket,在System V和其他的UNIX系统中也提供了socket编程界面。Windows系统中也有类似的Winsock接口,几乎所有函数都兼容。

最流行的网络协议是TCP/IP,socket提供的编程接口可以使用TCP/IP协议。BSD UNIX设计的socket编程界面是一种通用的网络编程界面,充分考虑了各种网络,例如:IPX网络,X.25网络,ISO的传输层协议TP4,IBM的SNA,以及UNIX内的进程之间通信等。但是,许多系统并不提供所有这些网络支持,有的系统甚至只允许TCP/IP协议的socket编程。

在运行TCP/IP协议的计算机中,一般都支持软件虚拟的IP数据报自环接口loopback。如果试图和IP地址等于自己地址的计算机通信,或者与地址127.0.0.1的计算机通信,数据不会发送到网络上,而是通过内核实现的虚拟的自环接口loopback,将数据回环到计算机自身。利用这一点,可以在一个计算机的多个进程之间进行通信。

现代的许多软件设计,同一台计算机内的进程之间通信也使用socket方式,这使得系统有很大的灵活性,因为需要的时候,只要将两个进程分布到不同的计算机上运行就可以了,而不需要更改程序。

1.2 TCP和UDP协议

TCP/IP对应用程序提供的服务主要有两种:

1、一种是面向连接的可靠的数据流传输TCP,另一种是无连接不可靠数据报传输UDP。

2、应用程序员在使用TCP/IP网络编写通信程序之前,应当首先在TCP和UDP协议之间作出选择,它们决定了由系统提供的通信可靠性。

1.3 基本网络体系结构

在ISO(International organization for Standards)定义的网络体系结构OSI(Open System Interconnection)开放系统互连模型中,计算机网络被定义为七层结构模型。从底向上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。目前广泛使用的Internet协议簇结构表示如下:

介质层包括IEEE802.3(Ethernet)、802.4(token bus)、802.5(token ring)及其它;

网络层以Internet protocal为主,ARP、RARP、ICMP和IGMP为辅结构;

传输层主要分为TCP协议和UDP协议;

应用层建立了以TCP为基础的FTP、TELNET、SMTP、SNMP,DNS(Domain Name Server)则建立在TCP和UDP之上,另外一些如RPC、NFS、XDR建立在纯粹的UDP协议之上。

计算机网络的通信过程不属于本文讨论范围,如有必要可自行查阅相关文献。

1.4 网络字节次序

不同计算机厂商在计算机内部存储整数的方法会有不同。有的厂商将整数的低位字节放在最低地址处,这种安排叫Little Endian,而有的厂商正好相反,高位字节放在内存的低地址处,它们的字节顺序安排叫Big Endian。

网络通信时,总是从内存的低地址开始传输连续的若干字节,因此,网络软件为了保证各计算机之间的互联性,要求所有的数据按统一的字节顺序传输,这就是规定的网络字节顺序。网络字节顺序的规定与Big Endian 相同。

UNIX中htons,htonl两个库函数,分别将短整数和长整数从主机字节次序转换到网络字节次序。相应地,ntohs和ntohl把网络字节顺序转换到主机字节顺序。

在socket的网络系统调用和库函数的结构体参数中的整数,一般也要求按网络字节顺序排列。为了源程序的可移植性,即使所用的UNIX中主机字节顺序与网络字节顺序正好吻合,也不要省略掉所必须的htons、htonl、ntohs和ntohl。

2 TCP客户端/服务器程序

2.1 Clinet/Server结构概述

socket编程接口,无论使用TCP还是UDP协议,都是一种client/server风格的软件结构。client/server结构的协议软件包括客户端软件和服务器端软件。

以文件传送协议FTP为例,UNIX提供的ftp命令就是客户端软件,在提供文件传送服务的远程计算机上,运行服务器端软件。这些服务器端的软件,在UNIX中由“精灵(daemon)”进程inetd控制,当TCP连接到达时,inetd创建ftpd服务进程负责与客户端软件的ftp通信,以完成文件传送操作,文件传送结束后,ftpd进程结束。

事实上,UNIX设计的socket机制,不仅仅是面向TCP/IP协议的,而是面向所有的网络通信,包括SPX/IPX,X.25,SNA,甚至充分考虑了可扩展性,支持将来可能出现的其他协议。IPv6比socket出现得晚,但是仍然继续沿用socket机制的这层“壳”,内核扩充了IPv6支持后,应用程序员就可以写基于IPv6的socket风格的通信程序了。

2.2 仿照文件的操作模式

访问文件有一组函数,而且这些函数调用的先后顺序也有一定的规则:

先open得到文件描述符fd,然后可以执行read和write访问文件内容,另有一些可以施加在fd上的函数,如:lseek定位文件指针,fcntl设置close-on-exec标志或者对文件的记录加锁,fstat获得文件的状态,等等。

使用管道时,就不再用open获得文件描述符,而是用pipe一次获得两个文件描述符。

使用socket的情况类似:

先用socket创建一个文件描述符,在这个文件描述符上,先施行一些connect,bind,listen等操作控制建立TCP连接,然后才能使用read和write收发数据。

通信过程中可使用fcntl、setsockopt和getpeername等函数,获得一些通信的状态,或者设置一些与通信有关的参数。最终,用close关闭连接。

这些函数调用的先后顺序,也遵循一定的规则。

2.3 网间进程通信需解决的问题

1、网间进程标识问题

单机上不同进程可以用进程号唯一标识。但在网络环境下,各主机独立分配的进程号是不能唯一标识该进程的。

2、多重协议的识别问题

操作系统支持的网络协议众多,不同协议的工作方式、地址格式都不同, TCP/IP协议引入了下列几个概念解决多重协议的识别问题:端口、地址、网络字节序、连接、半相关、全半相关等。

接下来我们将对以上概念逐一做一介绍。

2.3.1 Ports

把网络地址和端口号信息放在一个结构中,也就是套接字地址结构。大多数套接字系统调用都需要一个指向套接字地址结构的指针作为参数,并以此来传递地址信息。每个协议族都定义它自己的套接字地址结构,套接字地址结构都以“sockaddr_”开头,并以每个协议族名中的两个字母作结尾。

端口包括一些数据结构和I/O缓冲区。进程通过系统调用和某端口建立连接后,传输层传给该端口的数据都被相应的进程接收,相应进程发给传输层的数据都从该端口输出。

在TCP/IP协议的实现中,端口操作类似于一般I/O操作,进程获取一个端口相当于获取本地的唯一的I/O文件,可以用一般的读/写原语访问。类似于文件描述符,每个端口都有一个叫端口号的整数描述符,以区别不同的端口。由于TCP/IP传输层的两个协议TCP和UDP是两个完全独立的软件模块,因此各自端口号也相互独立。如TCP和UDP的端口号可以相同,两者并不冲突。

2.3.2 端口的分配

(1)全局分配:是一种集中式分配,由一个公认的中央机构根据用户的需要进行统一分配,并将结果公布于众;

(2)本地分配( 动态连接): 进程在需要访问传输层时,向本地操作系统提出申请,操作系统返回本地唯一的端口号,进程再通过合适的系统调用将自己和该端口连接起来(binding)。

TCP/IP端口号分配综合了两种方式。 TCP/IP是将端口号分为两部分,少量的作为保留端口(<256),以全局的方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口叫周知口,即使在不同的机器上,其端口号也相同。如HTTP中80、TELNET中的23等。剩余的为自由端口,是以本地方式进行分配。

/etc/services文件包含了服务名、端口号和协议名。若机器提供新服务,则需要在该文件中建立一项。

2.3.3 地址

网络通信中的两个进程是分别在两个不同的机器上。两台机器可以位于不同的网络,这些网络通过互连设备(网关、网桥、路由器)连接,因此需要三级寻址(网络地址、主机地址、进程标识)。

某一主机可与多个网络相连,必须指定一特定网络地址;

网络上每一台主机应有其唯一的地址;

每一主机上的每一进程应有在该主机上的唯一标识符。

通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。

2.3.4 连接

两个进程间的通信链路称为连接,连接在内部表现为一些缓冲区和一组协议机制。

2.3.5 半相关和全半相关

1、半相关(half-association)

网络中用一个三元组(the triple)可以在全局中唯一标识一个进程(协议,本机地址,本地端口号)。这样的一个三元组叫做半相关。

2、全半相关

一个完整的网间进程通信需要两个进程组成,并且只能使用同一种高层协议,也就是说TCP无法和UDP通信,因此一个完整的网间进程通信需要一个五元组来标识(协议,本机地址,本地端口号,远地地址,远地端口号)。

这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。

2.3.6 服务方式

1、面向连接服务

面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号(connect ID)。本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。

2、无连接服务

无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。

2.3.7 客户机/服务器模式

TCP/IP允许程序员在两个应用程序之间建立通信并来回传送数据,提供一种对等通信,可以是同一台机器上,也可以是不在同一台机器上。TCP/IP指明了数据是如何进行通信的,但并没有规定如何组织这些应用程序。

实践中网间进程通信的主要模式是客户机/服务器模式,即客户机向服务器发出请求,服务器在接收到请求后提供相应的服务。客户与服务器的作用是非对称的,服务器进程一般是先于客户启动,并一直随系统运行而存在,直到被终止。

客户机/服务器模式的建立基于以下两点:

(1)网络的目的是共享,从而让拥有众多资源的主机提供服务,让资源较少的客户请求服务;

(2)网间进程通信完全是异步的,在通信的进程间需要一种机制建立联系,以便为二者的数据交换提供同步。

客户机/服务器模式操作过程中采取的是以下主动请求方式:

1、服务器端

(1)打开一个通信信道,并告知本地主机将在某一公共地址端口(如Http中80、Telnet中的23)上接受用户的请求;

(2)等待客户请求到达端口;

(3)若接收到重复请求服务,则处理该请求并发送应答信号;若接收到并发服务请求,则要建立子进程来处理这个客户的请求,服务完成后,关闭子进程与客户的通信链路,并终止子进程;

(4) 跳至(2)步;

(5) 关闭服务器。

2、客户端

(1)打开一个通信信道,并连接到服务器所在主机的特定端口;

(2)向服务器发出服务器请求报文,等待并接受应答;

(3)请求结束后关闭与服务器的通信链路并终止此进程。

从以上描述过程可以看出:

客户与服务器的作用是非对称的,因此编码不同。

服务器进程一般是先于客户请求启动。只要系统运行,进程就一直存在,直到正常终止或者强迫终止。

客户软件不必处理并发性,因此比服务器程序简单得多。

2.3.8 TCP协议的实现机制

UDP协议是无连接的不可靠的协议,而TCP是面向连接的,所谓面向连接,是指在数据传输开始前建立一个数据通道,这个通道在整个数据传输过程中都保证通畅,到传输结束才关闭这一通道。

一个典型的TCP协议双方通信的过程是:

(1)获得对方IP地址和端口号。

(2)在本地主机上选择一个IP地址和端口号。

(3)建立连接。

(4)传输数据 这时数据就好象是直接从发送方顺序流出到接收方的一样,与普通的文件流操作没有什么不同。

(5)断开连接。

2.4 TCP网络通信程序构建

2.4.1 建立TCP连接

为了建立一条可靠的连接,TCP采用3次握手:

(1)服务器首先执行被打开的连接操作:socket, bind, listen, accept,然后服务器阻塞,等待客户端的连接。

(2)客户端执行主动打开的连接操作:socket,connect,同时客户端向服务器发送SYN类型的数据段,其中包括客户端的序列号。

(3)服务器接收到这个SYN数据段后,也发送一个SYN类型的数据段,其中包括服务器的序列号和对上一个SYN的确认。

(4)客户端在接收到这个确认后,就发送了对服务器SYN的确认,完成客户端的连接。

(5)服务器接收到这个确认,完成服务器的连接。

2.4.2 面向连接的SOCKET编程

套接字根据使用的协议不同可以分很多种,这里主要介绍两种:TCP套接字和UDP套接字。

Socket套接字对于不同的对象存在相应合理的解释。对于内核来说,socket标记了通信的终点,而对于应用程序来说,socket是一个文件描述符,这种文件描述符指定了应用程序写入/读取信息的位置。

显然,套接字描述符和文件描述符形式上基本一致,但数据结构上存在很大区别。

文件描述符在前文中已经有详细讨论,我们曾提到每个进程均含有一张文件描述符表,表内的数据结构存储了文件的相关信息。

套接字接口为网络通信增加了一个新的抽象,即套接字。当进程调用socket后,操作系统就分配了一个新的数据结构以便保存通信所需的信息,并在文件描述符表中填入了一个新的条目,该条目含有一个指向这个数据结构的指针。尽管套接字内部的数据结构有许多字段,在系统创建套接字后,大多数字段中的值并没有填上。在套接字能够被使用之前,创建该套接字的应用程序必须用其他系统调用把套接字数据结构中的这些信息填上。

套接字一旦创建,应用程序就必须指定如何使用它,套接字本身是完全通用的,可以用来进行任意方式的通信。例如,服务器可以将套接字配置为等待传入连接,而客户可以将其配置为发起连接。

如果服务器将套接字配置为等待传入连接,就称此套接字套接字为主动套接字;反之,客户用来发起连接的套接字就称为被动套接字。

使用TCP的客户和服务器各自使用套接字的一种调用序列。

客户创建套接字,调用connect连接服务器,交互时,使用send(或者write)发送请求,使用recv(或者read)接收应答。当使用连接结束时,客户调用close。

服务器使用bind指明使用的本地(熟知)协议端口,调用listen设置连接等待队列的长度,之后便进入循环。在循环中,服务器调用accept进行等待,直到下一个连接请求到达为止,它使用recv和send(或read和write)同客户交流,最后使用close终止连接。之后,服务器回到accept调用,在那里等待下一个连接。

2.4.3 使用TCP时客户和服务端通信流程

服务端:socket->bind->listen->accept->read->write->close

客户端:socket->connect->write->read->close

2.4.4 套接字用到的具体数据结构

1、通用套接字地址数据结构(/usr/include/sys/socket.h)

struct sockaddr /struct to hold an address /

{

unsigned short sa_familly; /address family/

char sa_data[14]; /protocol address/

};

sa_familly为协议族,指出通信协议类型,对于internet域的地址族为AF-INET。

sa_data存贮实际的地址。

2、IPV4套接字地址数据结构

在实际中为了方便处理,每个协议在上面通用定义的基础上改成自己的套接字地址结构,这些结构均以“sockaddr_”开头,并以对应每个协议族的唯一后缀结束。

对于我们关心的Internet(IPV4)域,我们有专用的套接字地址结构sockaddr_in结构,它定义在中。

struct sockaddr_in

{

short int sin_fammily; / address family Ipv4 is AF_INET/

unsigned short sin_port; /port number/

struct in_addr sin_addr; /internet address IP address/

unsigned char sin_zero[8]; /same size as struct sockaddr/

};

struct in_addr

{

unsigned long s_addr; /32-bit IP address , network byte/

};

2.4.5 套接字用到的基本系统调用

1、socket系统调用,用来获得一个socket描述符。

include

include

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

domain是指存放通信进程的区域,通常使用的domains包括:

AF_UNIX for communication between processes on one system;

AF_INET (IPv4) for communication between processes on the same or different systems using the DARPA standard protocols(IP/UDP/TCP)

AF_INET6 (IPv6)

AF_LOCAL (Unix domain)

type:通信的类型SOCK-STREAM(TCP)、SOCK-DGRAM(UDP)、SOCK-RAW。

Protocol:一般为0,除非使用原始套接口。

2、bind系统调用

bind为套接字指定本地地址,它包含了IP地址和协议端口号,服务器主要由bind来指明熟知的端口号,它在此熟知的端口号等待连接。以下是一段bind程序演示:

include

include

include

define MYPORT 3490

main()

{ int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); /需要错误检查 / my_addr.sin_family = AF_INET; / host byte order / my_addr.sin_port = htons(MYPORT); / short, network byte order / my_addr.sin_addr.s_addr = inet_addr(“132.241.5.10”); bzero(&(my_addr.sin_zero),; / zero the rest of the struct / / don’t forget your error checking for bind(): / bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

处理自己的 IP 地址和端口的 时候,有些工作是可以自动处理的。 my_addr.sin_port = 0; / 随机选择一个没有使用的端口 / my_addr.sin_addr.s_addr = INADDR_ANY; / 用自己的IP地址 / 通过将0赋给 my_addr.sin_port,告诉 bind() 自己选择合适的端 口。

将 my_addr.sin_addr.s_addr 设置为 INADDR_ANY,告诉 它自动填上它所运行的机器的 IP 地址。没有将INADDR_ANY转 换为网络字节顺序!INADDR_ANY 实际上就 是 0!即使你改变字节的顺序,0依然是0。 my_addr.sin_port = htons(0); / 随机选一没有使用的端口 / my_addr.sin_addr.s_addr = htonl(INADDR_ANY);

/ 使用自己的IP地址 /

3、connect系统调用

connect系统调用于在一个指定的socket上建立一个连接。

int connect(int sockfd, struct sockaddr *serv_addr,int addrlen);

在创建套接字后,客户程序connect以便同远程服务器建立主动的连接,connect的一个参数允许客户指明远程端点,它包括IP地址以及协议端口号。一旦建立了连接,客户就可以向它传送数据了。

include

include

include

define DEST_IP “132.241.5.10”

define DEST_PORT 23

main()

{

int sockfd;

struct sockaddr_in dest_addr; / 目的地址/ sockfd = socket(AF_INET, SOCK_STREAM, 0); / 错误检查 / dest_addr.sin_family = AF_INET; / host byte order /

dest_addr.sin_port = htons(DEST_PORT); / short, network byte order / dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);

bzero(&(dest_addr.sin_zero),; / zero the rest of the struct / / don’t forget to check error / connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));

4、listen系统调用

系统调用使一个套接字进入监听状态,仅被TCP服务器调用

include

int listen(int sockfd, int backlog);

在创建套接字后,直到应用程序采取进一步行动以前,它既不是主动的(由客户使用)也不是被动的(由服务器使用)。面向连接的服务器用listen将一个套接字置为被动的模式,并使其准备接受传入连接。大多数服务器由无限循环构成。该循环可以接受传入的下一个连接,然后对其进行处理,完成后便返回准备接受下一个连接,正处于忙的服务器有可能又来了一个连接请求。为保证不丢失连接请求,服务器必须给listen传递一个参数,告诉操作系统对某个套接字上的连接请求进行排队。因此,listen的一个参数指明某个套接字将被置为被动的模式,而另一个参数将指明套接字所使用的队列长度。

5、accept系统调用

int accept(int sockfd, struct sockaddr addr, int addrlrn);

accept返回非负描述字表示成功,出错将返回-1。如果成功,则返回值用来标识新建立的连接。

参数addr为返回客户进程协议地址,参数addrlen为返回客户进程协议地址的长度。

对TCP套接字,服务器用socket创建一个套接字,用bind指明本地端口地址,用listen将其置为被动的模式,用accept以获取传入连接请求,accept的一个参数指明一个套接字,将从该套接字上接受连接。

accept为每一个新连接创建了一个新的套接字,并将这个新套接字的描述符传给调用者。服务器只对这个新的连接使用该套接字,而原来的套接字接受其他的连接请求。服务器一旦接受了一个连接后,它就可以在这个新的套接字上传送数据。在使用完这个新的套接字后,服务器将关闭该套接字。

include

include

include

define MYPORT 3490 /用户接入端口/

define BACKLOG 10 / 多少等待连接控制/

main()

{

int sin_size; int sockfd, new_fd; / listen on sock_fd, new connection on new_fd / struct sockaddr_in my_addr; / 地址信息 / struct sockaddr_in their_addr; / connector’s address information / sockfd = socket(AF_INET, SOCK_STREAM, 0); / 错误检查/ my_addr.sin_family = AF_INET; / host byte order / my_addr.sin_port = htons(MYPORT); / short, network byte order / my_addr.sin_addr.s_addr = INADDR_ANY; / auto-fill with my IP / bzero(&(my_addr.sin_zero),; / zero the rest of the struct / / don’t forget your error checking for these calls: / bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, &their_addr, &sin_size);

6、send和recv系统调用(read和write)

int send( int sockfd,void *buf ,int len,int flags);

int recv( int sockfd,void *buf ,int len,int flags);

在大多数UNIX系统,程序员可以用read代替recv,用write代替send。对TCP和UDP套接字来说,他们的语义是一样的。把 flags 设置为 0 就可以了。

下列调用将导致进程阻塞:

1、accept()系统调用,若没有连接请求,则被阻塞。

2、read()系统调用。

3、write()系统调用。

7、Closesocket