基于TCP/IP的网络编程接口:Socket

 
 
 
 

1.4 基于TCP/IP的网络编程接口:Socket
在后面的章节中,将会对一些源代码进行分析,包括Ping命令(用来测试与目标主机之间的通信)的实现、端口扫描的实现、Sniffer(监听网络上传输的信息,如用户名和口令等)的实现等。如果要深刻分析这些源代码,首先得了解基于TCP/IP协议的网络编程接口:Socket。
Socket接口是TCP/IP传输层的应用编程接口(API),Socket接口定义了许多函数和例程,程序员可以用它们来开发基于TCP/IP协议的应用程序,如文件传输、聊天室、网络监听等。大家都知道,TCP/IP协议最早是在Unix系统中实现的,因此,Socket接口最先也是在Unix系统中实现。那么,Socket到底是什么呢?首先了解一下文件的概念。在Unix中,进程要对文件进行操作,一般使用open调用打开一个文件进行访问,每个进程都有一个文件描述符表,存放打开的文件描述符。用户使用open调用得到的文件描述符实际上是文件描述符在该表中的索引号,该表项的内容是一个指向文件表的指针。应用程序只要使用该描述符就可以对指定文件进行操作。
同样,Socket接口增加了网络通信操作的抽象定义,与文件操作一样,每个打开的Socket都对应一个整数,一般称为Socket描述符,指向一个与该Socket有关的数据结构。一旦建立了一个Socket,应用程序可以使用其他调用来实现基于网络的通信。
1.4.1 基本概念
对于Socket编程,还需要了解以下基本概念。
1.端口
端口是网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(Protocol Port,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接并绑定(Binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,端口操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地惟一的I/O文件,可以用一般的读写原语进行访问。
类似于文件描述符,每个端口都拥有一个叫端口号(Port Number)的整数型标识符,用于区别不同端口。每个端口都标识了一种服务,如FTP服务端口为21。
端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地惟一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑定)。TCP/IP端口号的分配政策综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口(0~1023),以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知端口,Well-known Port),即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。如WWW服务的周知端口为80,Telnet服务的周知端口为23。表1.1列出了Internet常用服务的标准端口号。

表1.1 Internet常用服务的标准端口号
Internet服务 标准端口号 Internet服务 标准端口号
FTP 21 Finger 79
Telnet 23 WWW 80
SMTP 25 POP3 110
Whois 43 NNTP 119
Gopher 70 TALK 517
2.地址
网络通信中通信的两个进程分别在不同的机器上。在互联网络中,两台机器可能位于不同的网络,这些网络通过网络互联设备(网桥,路由器,网关等)连接。因此需要三级寻址:
(1)某一主机可与多个网络相连,必须指定所在的网络地址;
(2)网络上每一台主机应有其惟一的主机地址; 
(3)每一主机上的每一进程应有在该主机上的惟一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示(前面所说的四类地址)。TCP和UDP均使用16位端口号(216,端口总数为65536)标识用户进程。 
3.半相关
用一个三元组可以在网络中全局惟一地标志一个进程:
(协议,本地地址,本地端口号)
这样的三元组,叫做一个半相关(Half-association),它指定连接的半部分,即连接的一方。
4.全相关
一个完整的网络间的进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:
(协议,本地地址,本地端口号,远地地址,远地端口号)
这样的五元组,叫做一个相关,即两个协议相同的半相关才能组合成一个合适的相关,或完全指定一连接。
5.服务方式
在网络体系结构中,各层次的分工和协作集中体现在相邻层之间的界面上。"服务"是描述相邻层之间关系的抽象概念,即网络中各层向相邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语,如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。
在OSI的术语中,网络层及其以下各层又称为通信子网,只提供点到点通信,没有程序或进程的概念。而传输层实现的是"端到端"通信,引进网络间进程通信概念,同时也要解决差错控制,流量控制,连接管理等问题,为此提供不同的服务方式:面向连接(虚电路)和无连接
(1) 面向连接服务:类似于电话系统的服务模式,即每一次完整的数据传输都要经过建立连接、使用连接、释放连接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号。本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。
(2) 无连接服务:类似于邮政系统的服务模式,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。
6.顺序
在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同。"顺序"是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。
7.差错控制
保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验"检查和(Checksum�"的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。
8.流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。
9.字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。
10.数据报
数据报在传输过程中不保证顺序,报文具有边界。UDP协议提供数据报服务。
11.全双工/半双工
全双工是指一旦通信连接建立后,双方可以同时进行数据发送。
半双工是指同一时刻只能有一方进行数据发送。
12.缓存/带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用流控制协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。
在数据传送过程中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息,如Unix系统的中断键(Control-C),称为带外数据。逻辑上看,好像用户进程使用了一个独立的通道传输这些数据。该通道与每对连接的流相联系。
1.4.2 客户机/服务器模式
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机/服务器模式(Client/Server mode),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。
那么,为什么需要建立客户机/服务器模式呢?这是因为:
首先,建立网络的起因是需要共享网络中软硬件资源、运算能力和信息等,使拥有众多资源的主机为客户机提供服务,资源较少的客户机请求服务,这是一个非对等的关系。
其次,网间进程通信是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步。
客户机/服务器模式在操作过程中采取的是主动请求方式,下面分别给出服务器和客户机的一般流程。
服务器:先启动,并根据请求提供相应服务:
(1)打开一通信通道并告知本地主机,它愿意在某一周知端口(如FTP为21)上接收客户请求;
(2)等待客户请求到达该端口;
(3)接收到客户请求,处理该请求并发送应答信号。如果收到并发的服务请求(即在同一时刻,多个客户同时向服务器发出请求),则激活一新进程来处理这个客户请求(如Unix系统中用fork、exec)。新进程处理此客户请求,并不需要对其他请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止;
(4)转(2),继续等待另一客户请求;
(5)关闭服务器。
客户机:后启动,向服务方发送请求: 
(1)打开一通信通道,并连接到服务器所在主机的特定端口;
(2)向服务器发服务请求,等待并接收应答;继续提出请求......
(3)请求结束后关闭通信通道并终止。
从上面所描述过程可知:
(1)客户机/服务器模式的本质是将一个应用分成两个部分,一部分在服务器完成,一部分在客户机完成。在这种模式中,一般将复杂的数值计算或者各种服务的处理放在服务器运行,将数据的表示等放在客户机进行。例如邮件系统,真正的邮件发送等服务都是在服务器进行,客户机仅仅是查看邮件或者提出发送请求,并将邮件传送到邮件发送服务器;
(2)客户与服务器进程的作用是非对称的,因此编码不同;
(3)服务进程一般是先于客户请求而启动。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
1.4.3 Socket类型及其工作流程
Socket有三种类型:流式套接口,数据报式套接口及原始式套接口。
(1) 流式套接口(SOCK_STREAM)
又称流式套接字,提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)就是使用流式套接口。
(2) 数据报式套接口(SOCK_DGRAM)
又称数据报式套接字,数据报套接口定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。网络文件系统(NFS)使用数据报式套接口。
(3) 原始式套接口(SOCK_RAW)
又称原始式套接字,该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相互作用。若使用无连接的套接口编程,程序流程可以用图1.5表示。

图1.5 无连接套接口应用流程图

图1.6 面向连接套接口应用流程图
面向连接服务器处理的请求往往比较复杂。使用面向连接的套接口编程,可以用图1.6来表示。面向连接的套接口工作过程如下:服务器首先启动,通过调用Socket( )建立一个套接口,然后调用bind( )将该套接口和本地网络地址联系在一起,再调用listen( )使套接口做好侦听的准备,并规定它的请求队列的长度,之后就调用accept( )来接收连接。客户在建立套接口后就可调用connect( )和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用read( )和write( )来发送和接收数据。最后,待数据传送结束后,双方调用close( )关闭套接口。
1.4.4 基本套接口系统调用
表1.2列出了常用的Socket调用函数(按照字母顺序)。
表1.2 常用的Socket调用函数


*表示例程在某些情况下可能会阻塞。
下面给出几个基本套接口系统调用的说明。其他调用的详细说明请参阅有关Socket编程书籍。
1.创建套接口──Socket( )
应用程序在使用套接口前,首先必须创建一个套接口,其调用格式如下:
int Socket( int af, int type, int protocol );
入口参数:af、type、protocol。参数af指定通信使用的区域,Unix系统支持AF_Unix、AF_INET、AF_NS等,而DOS、Windows中仅支持AF_INET,它是网际网区域。参数type 描述要建立的套接口的类型(流式、数据报式和原始式套接口)。参数protocol说明该套接口使用的协议(TCP、UDP、RAW),如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接口,并将相应的资源分配给它,同时返回一个整型套接口号。因此,Socket()系统调用实际上指定了相关五元组中的"协议"这一元。
2.指定本地地址──bind( )
当一个套接口用Socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接口地址(包括本地主机地址和本地端口地址)与所创建的套接口号联系起来,即将名字赋予套接口,以指定本地半相关。其调用格式如下:
int bind( SOCKET s, const struct sockaddr FAR * name, int namelen );
参数s是由Socket( )调用返回的并且未作连接的套接口描述符(套接口号)。参数name 是赋给套接口s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。
如果没有错误发生,bind()返回0。否则返回值SOCKET_ERROR。
地址结构在建立套接口通信过程中起着重要作用,作为一个网络应用程序设计者对套接口地址结构必须有明确认识。例如,Unix BSD有一组描述套接口地址的数据结构,其中使用TCP/IP协议的地址结构为:
struct sockaddr_in{
short sin_family; /*AF_INET*/
u_short sin_port; /*16位端口号,网络字节顺序*/
struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/
char sin_zero[8]; /*保留*/
}
3.建立套接口连接──connect( )与accept( )
这两个系统调用用于建立一个完整相关,其中connect( )用于客户方建立连接。无连接的套接口进程也可以调用connect( ),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接口建立"连接",便能判断该端口不可操作。而accept( )用于服务器等待来自某客户进程的实际连接。
connect( )的调用格式如下:
int connect( SOCKET s, const struct sockaddr FAR * name, int namelen );
参数s是欲建立连接的本地套接口描述符。参数name指出说明对方套接口地址结构的指针。对方套接口地址长度由namelen说明。
如果没有错误发生,connect( )返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。
由于地址族总被包含在套接口地址结构的前两个字节中,并通过Socket( )调用与某个协议族相关。因此bind( )和connect( )无需协议作为参数。
accept( )的调用格式如下:
int accept( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );
参数s为本地套接口描述符,在用作accept( )调用的参数前应该先调用过listen( )。addr 是指向客户方套接口地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接口创建时建立的地址族决定。addrlen 为客户方套接口地址的长度(字节数)。如果没有错误发生,accept( )返回一个SOCKET类型的值,表示接收到的套接口的描述符。否则返回值INVALID_SOCKET。
accept( )用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept( )后,服务器等待从编号为s的套接口上接受客户连接请求,而连接请求是由客户方的connect( )调用发出的。当有连接请求到达时,accept( )调用将请求连接队列上的第一个客户方套接口地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接口号。新的套接口可用于处理服务器并发请求。
四个套接口系统调用Socket( )、bind( )、connect( )、accept( ),可以完成一个完全五元相关的建立。Socket( )指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind( )指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind( );在客户方,若采用面向连接,则可以不调用bind( ),而通过connect()自动完成。若采用无连接,客户方必须使用bind( )以获得一个惟一的地址。
以上讨论仅对客户/服务器模式而言,实际上套接口的使用是非常灵活的,惟一需遵循的原则是进程通信之前,必须建立完整的相关。
4.监听连接──listen( )
此调用用于面向连接服务器,表明它愿意接收连接,并设置接收队列的最大长度。listen( )需在accept( )之前调用,其调用格式如下:
int listen( SOCKET s, int maxQue );
参数s标识一个本地已建立、尚未连接的套接口号,服务器愿意从它上面接收请求。maxQue表示请求连接队列的最大长度,用于限制排队请求的个数。如果没有错误发生,listen()返回0;否则它返回SOCKET_ERROR。
listen( )在执行调用过程中可为没有调用过bind()的套接口s完成所必须的连接,并建立长度为maxQue的请求连接队列。 
调用listen( )是服务器接收一个连接请求的四个步骤中的第三步。它在调用Socket( )分配一个流套接口,且调用bind( )赋给s一个名字之后调用,而且一定要在accept( )之前调用。
5.数据传输──send( )与recv( )
当一个连接建立以后,就可以进行数据传输。常用的系统调用有send( )和recv( )。
send( )调用用于在参数s指定的已连接的数据报或流套接口上发送输出数据,格式如下:
int send( SOCKET s, const char FAR *buf, int len, int flags );
参数s为已连接的本地套接口描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send( )返回总共发送的字节数。否则它返回SOCKET_ERROR。
recv( )调用用于在参数s指定的已连接的数据报或流套接口上接收输入数据,格式如下:
int recv( SOCKET s, char FAR *buf, int len, int flags );
参数s 为已连接的套接口描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv( )返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
6.关闭套接口──closeSocket( )
closeSocket( )关闭套接口s,并释放分配给该套接口的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closeSocket( )的调用格式如下:
int closeSocket( SOCKET s );
参数s是待关闭的套接口描述符。如果没有错误发生,closeSocket( )返回0。否则返回值SOCKET_ERROR。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值