Socket编程原理

Linux的兴起可以说是Internet创造的一个奇迹。Linux作为一个完全开放其原代码的免费的自由软件,兼容了各种UNIX标准(如 POSIX、UNIX System V 和 BSD UNIX 等)的多用户、多任务的具有复杂内核的操作系统。在中国,随着Internet的普及,一批主要以高等院校的学生和ISP的技术人员组成的Linux爱好者队伍已经蓬勃成长起来。越来越多的编程爱好者也逐渐酷爱上这个优秀的自由软件。本文介绍了Linux下Socket的基本概念和函数调用。

2、 什么是Socket

Socket(套接字)是通过标准的UNIX文件描述符和其它程序通讯的一个方法。每一个套接字都用一个半相关描述:{协议,本地地址、本地端口} 来表示;一个完整的套接字则用一个相关描述:{协议,本地地址、本地端口、远程地址、远程端口},每一个套接字都有一个本地的由操作系统分配的唯一的套接字号。

3、 Socket的三种类型

(1) 流式Socket(SOCK_STREAM)

流式套接字提供可靠的、面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序的。

(2) 数据报Socket(SOCK_DGRAM)

数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠、无差错。它使用数据报协议UDP

(3) 原始Socket

原始套接字允许对底层协议如IP或ICMP直接访问,它功能强大但使用较为不便,主要用于一些协议的开发。

4、 利用套接字发送数据

1、 对于流式套接字用系统调用send()来发送数据。

2、对于数据报套接字,则需要自己先加一个信息头,然后调用sendto()函数把数据发送出去。

5、 Linux中Socket的数据结构

(1) struct sockaddr { //用于存储套接字地址

unsigned short sa_family;//地址类型

char sa_data[14]; //14字节的协议地址

};

(2) struct sockaddr_in{ //in 代表internet

short int sin_family; //internet协议族

unsigned short int sin_port;//端口号,必须是网络字节顺序

struct in_addr sin_addr;//internet地址,必须是网络字节顺序

unsigned char sin_zero;//添0(和struct sockaddr一样大小

};

(3) struct in_addr{

unsigned long s_addr;

};

6、 网络字节顺序及其转换函数

(1) 网络字节顺序

每一台机器内部对变量的字节存储顺序不同,而网络传输的数据是一定要统一顺序的。所以对内部字节表示顺序与网络字节顺序不同的机器,一定要对数据进行转换,从程序的可移植性要求来讲,就算本机的内部字节表示顺序与网络字节顺序相同也应该在传输数据以前先调用数据转换函数,以便程序移植到其它机器上后能正确执行。真正转换还是不转换是由系统函数自己来决定的。

(2) 有关的转换函数

* unsigned short int htons(unsigned short int hostshort):

主机字节顺序转换成网络字节顺序,对无符号短型进行操作4bytes

* unsigned long int htonl(unsigned long int hostlong):

主机字节顺序转换成网络字节顺序,对无符号长型进行操作8bytes

* unsigned short int ntohs(unsigned short int netshort):

网络字节顺序转换成主机字节顺序,对无符号短型进行操作4bytes

* unsigned long int ntohl(unsigned long int netlong):

网络字节顺序转换成主机字节顺序,对无符号长型进行操作8bytes

注:以上函数原型定义在netinet/in.h里

7、 IP地址转换

有三个函数将数字点形式表示的字符串IP地址与32位网络字节顺序的二进制形式的IP地址进行转换

(1) unsigned long int inet_addr(const char * cp):该函数把一个用数字和点表示的IP地址的字符串转换成一个无符号长整型,如:struct sockaddr_in ina

ina.sin_addr.s_addr=inet_addr("202.206.17.101")

该函数成功时:返回转换结果;失败时返回常量INADDR_NONE,该常量=-1,二进制的无符号整数-1相当于 255.255.255.255,这是一个广播地址,所以在程序中调用iner_addr()时,一定要人为地对调用失败进行处理。由于该函数不能处理广播地址,所以在程序中应该使用函数inet_aton()。

(2)int inet_aton(const char * cp,struct in_addr * inp):此函数将字符串形式的IP地址转换成二进制形式的IP地址;成功时返回1,否则返回0,转换后的IP地址存储在参数inp中。

(3) char * inet_ntoa(struct in-addr in):将32位二进制形式的IP地址转换为数字点形式的IP地址,结果在函数返回值中返回,返回的是一个指向字符串的指针。

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wsguojun2008/archive/2011/02/11/6179037.aspx

1. 问题的引入 
UNIX系统的I/O命令集,是从Maltics和早期系统中的命令演变出来的,其模式为打开一读/写一关闭(open-write-read-close)。在一个用户进程进行I/O操作时,它首先调用“打开”获得对指定文件或设备的使用权,并返回称为文件描述符的整型数,以描述用户在打开的文件或设备上进行I/O操作的进程。然后这个用户进程多次调用“读/写”以传输数据。当所有的传输操作完成后,用户进程关闭调用,通知操作系统已经完成了对某对象的使用。

TCP/IP协议被集成到UNIX内核中时,相当于在UNIX系统引入了一种新型的I/O操作。UNIX用户进程与网络协议的交互作用比用户进程与传统的I/O设备相互作用复杂得多。首先,进行网络操作的两个进程在不同机器上,如何建立它们之间的联系?其次,网络协议存在多种,如何建立一种通用机制以支持多种协议?这些都是网络应用编程界面所要解决的问题。 

在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程界面──套接字(socket)在网络软件中被广泛应用,至今已引进微机操作系统DOS和Windows系统中,成为开发网络应用软件的强有力工具,本章将要详细讨论这个问题。

2. 套接字编程基本概念 
开始使用套接字编程之前,首先必须建立以下概念。 

2.1 网间进程通信 
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,如UNIX BSD中的管道(pipe)、命名管道(named pipe)和软中断信号(signal),UNIX system V的消息(message)、共享存储区(shared memory)和信号量(semaphore)等,但都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 

其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。

 

为了解决上述问题,TCP/IP协议引入了下列几个概念。 


端口 

网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。 

按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。 

端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,对端口的操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。 

类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。 

端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑扎)。TCP/IP端口号的分配中综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知口,well-known port),即使在不同的机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP均规定,小于256的端口号才能作保留端口。 

地址 

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

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

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

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

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

 

网络字节顺序 

不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低价先存),有的存高位字节(高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。 


连接 

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


半相关 

综上所述,网络中用一个三元组可以在全局唯一标志一个进程: 

(协议,本地地址,本地端口号) 

这样一个三元组,叫做一个半相关(half-association),它指定连接的每半部分。 


全相关 

一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:  (协议,本地地址,本地端口号,远地地址,远地端口号)  这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。 


2.2 服务方式 
在网络分层结构中,各层之间是严格单向依赖的,各层次的分工和协作集中体现在不同层之间的界面上。“服务”是描述不同层之间关系的抽象概念,即网络中各层向紧邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语(primitive),如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。网络中的n层总要向n+1层提供比n-1层更完备的服务,否则n层就没有存在的价值。在OSI的术语中,网络层及其以下各层又称为通信子网,只提供点到点通信,没有程序或进程的概念。而传输层实现的是“端到端”通信,引进网间进程通信概念,同时也要解决差错控制,流量控制,数据排序(报文排序),连接管理等问题,为此提供不同的服务方式: 


面向连接(虚电路)或无连接 

面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号(connect ID)。本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。  无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。 

下面给出这两种服务的类型及应用中的例子: 

服务类型             服 务                         例 子 

面向连接              可靠的报文流              文件传输(FTP)

                          可靠的字节流              远程登录(Telnet) 

                          不可靠的连接              数字话音

无连接                 不可靠的数据报           电子邮件(E-mail)

                          有确认的数据报           电子邮件中的挂号信

                          请求-应答                 网络数据库查询


顺序 

在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同。“顺序”是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。 


差错控制 

保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验“检查和(Checksum)”的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。 


流控制 

在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。 


字节流 

字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。 


报文 

接收方要保存发送方的报文边界。UDP协议提供报文服务。 


全双工/半双工 

端-端间数据同时以两个方向/一个方向传送。 


缓存/带外数据 

在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。 

在数据传送过程中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息,如UNIX系统的中断键(Delete或Control-c)、终端流控制符(Control-s和Control-q),称为带外数据。逻辑上看,好象用户进程使用了一个独立的通道传输这些数据。该通道与每对连接的流相联系。由于Berkeley Software Distribution中对带外数据的实现与RFC 1122中规定的Host Agreement不一致,为了将互操作中的问题减到最小,应用程序编写者除非与现有服务互操作时要求带外数据外,最好不使用它。 

 

 

2.3 客户/服务器模式 
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式(Client/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于不同的客户/服务器模式的TCP/IP。  客户/服务器模式在工作过程中采取的是主动请求方式: 

首先服务器方要先启动,并根据请求提供相应服务: 

1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如FTP为21)接收客户请求; 

2. 等待客户请求到达该端口; 

3. 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。 

4. 返回第二步,等待另一客户请求。 

5. 关闭服务器 


客户方: 

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

2. 向服务器发服务请求报文,等待并接收应答;继续提出请求...... 

3. 请求结束后关闭通信通道并终止。 


从上面所描述过程可知: 

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

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

2.4 套接字类型 
TCP/IP的socket提供下列三种类型套接字。 


流式套接字(SOCK_STREAM) 

提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。 


数据报式套接字(SOCK_DGRAM) 

提供了一个无连接服务。数据包以独立包形式被发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。 


原始式套接字(SOCK_RAW) 

该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备

。 

3. 基本套接字系统调用 
为了更好地说明套接字编程原理,下面给出几个基本套接字系统调用说明。 

3.1 创建套接字──socket() 
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下: 

SOCKET PASCAL FAR socket(int af, int type, int protocol); 

该调用要接收三个参数:af、type、protocol。参数af指定通信发生的区域,UNIX系统支持的地址族有:

AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET,它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。 

#include<sys/types.h>

#include<sys/socket.h>

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

参数domain指定要创建的套接字的协议族,可以是如下值:

AF_UNIX //UNIX域协议族,本机的进程间通讯时使用

AF_INET //Internet协议族(TCP/IP)

AF_ISO //ISO协议族

参数type指定套接字类型,可以是如下值:

SOCK_STREAM //流套接字,面向连接的和可靠的通信类型

SOCK_DGRAM //数据报套接字,非面向连接的和不可靠的通信类型

SOCK_RAW //原始套接字,只对Internet协议有效,可以用来直接访问IP协议

参数protocol通常设置成0,表示使用默认协议,如Internet协议族的流套接字使用TCP协议,而数据报套接字使用UDP协议。当套接字是原始套接字类型时,需要指定参数protocol,因为原始套接字对多种协议有效,如ICMP和IGMP等。

Linux系统中创建一个套接字的操作主要是:在内核中创建一个套接字数据结构,然后返回一个套接字描述符标识这个套接字数据结构。这个套接字数据结构包含连接的各种信息,如对方地址、TCP状态以及发送和接收缓冲区等等,TCP协议根据这个套接字数据结构的内容来控制这条连接。

 

 

3.2 指定本地地址──bind() 
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下: 

int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen); 

参数s是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。参数name 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。namelen表明了name的长度。 

如果没有错误发生,bind()返回0。否则返回值SOCKET_ERROR。 

地址在建立套接字通信过程中起着重要作用,作为一个网络应用程序设计者对套接字地址结构必须有明确认识。例如,UNIX BSD有一组描述套接字地址的数据结构,其中使用TCP/IP协议的地址结构为: 

view plaincopy to clipboardprint?
struct sockaddr_in{    
 
  short sin_family; /*AF_INET*/    
 
  u_short sin_port; /*16位端口号,网络字节顺序*/    
 
  struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/    
 
  char sin_zero[8]; /*保留*/    
 
}  
struct sockaddr_in{ 

  short sin_family; /*AF_INET*/ 

  u_short sin_port; /*16位端口号,网络字节顺序*/ 

  struct in_addr sin_addr; /*32位IP地址,网络字节顺序*/ 

  char sin_zero[8]; /*保留*/ 

}  

 

#include<sys/types.h>

#include<sys/socket.h>

int bind(int sockfd,struct sockaddr * myaddr,int addrlen);

参数sockfd是函数sockt返回的套接字描述符;参数myaddr是本地地址;参数addrlen是套接字地址结构的长度。执行成功时返回 0,否则,返回-1,并设置全局变量errno为错误类型EADDRINUSER。

服务器和客户机都可以调用函数bind来绑定套接字地址,但一般是服务器调用函数bind来绑定自己的公认端口号。绑定操作一般有如下几种组合方式:

表1

程序类型

IP地址

端口号

说明

服务器

INADDR_ANY

非零值

指定服务器的公认端口号

服务器

本地IP地址

非零值

指定服务器的IP地址和公认端口号

客户机

INADDR_ANY

非零值

指定客户机的连接端口号

客户机

本地IP地址

非零值

指定客户机的IP地址连接端口号

客户机

本地IP地址

指定客户机的IP地址

分别说明如下:

(1)服务器指定套接字地址的公认端口号,不指定IP地址:即服务器调用bind时,设置套接字的IP地址为特殊的INADDE-ANY,表示它愿意接收来自任何网络设备接口的客户机连接。这是服务器最常用的绑定方式。

(2)服务器指定套接字地址的公认端口号和IP地址:服务器调用bind时,如果设置套接字的IP地址为某个本地IP地址,这表示这台机器只接收来自对应于这个IP地址的特定网络设备接口的客户机连接。当服务器有多块网卡时,可以用这种方式来限制服务器的接收范围。

(3)客户机指定套接字地址的连接端口号:一般情况下,客户机调用connect函数时不用指定自己的套接字地址的端口号。系统会自动为它选择一个未用的端口号,并且用本地的IP地址来填充套接字地址中的相应项。但有时客户机需要使用一个特定的端口号(比如保留端口号),而系统不会未客户机自动分配一个保留端口号,所以需要调用函数bind来和一个未用的保留端口号绑定。

(4)指定客户机的IP地址和连接端口号:表示客户机使用指定的网络设备接口和端口号进行通信。

(5)指定客户机的IP地址:表示客户机使用指定的网络设备接口和端口号进行通信,系统自动为客户机选一个未用的端口号。一般只有在主机有多个网络设备接口时使用。

我们一般不在客户机上使用固定的客户机端口号,除非是必须使用的情况。在客户机上使用固定的端口号有以下不利:

(1)服务器执行主动关闭操作:服务器最后进入TIME_WAIT状态。当客户机再次与这个服务器进行连接时,仍使用相同的客户机端口号,于是这个连接与前次连接的套接字对完全一样,但是一呢、为前次连接处于TIME_WAIT状态,并未消失,所以这次连接请求被拒绝,函connect以错误返回,错误类型为ECONNREFUSED

(2)客户机执行主动关闭操作:客户机最后进入TIME_WAIT状态。当马上再次执行这个客户机程序时,客户机将继续与这个固定客户机端口号绑定,但因为前次连接处于TIME_WAIT状态,并未消失,系统会发现这个端口号仍被占用,所以这次绑定操作失败,函数bind以错误返回,错误类型为 EADDRINUSE。

 

 

3.3 建立套接字连接──connect()与accept() 
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接字建立“连接”,便能判断该端是否可操作。而accept()用于使服务器等待来自某客户进程的实际连接。 

connect()的调用格式如下: 

int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen); 

参数s是欲建立连接的本地套接字描述符。参数name指出说明对方套接字地址结构的指针。对方套接字地址长度由namelen说明。 

如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。 

由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。 

accept()的调用格式如下: 

SOCKET PASCAL FAR 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()以获得一个唯一的地址。 

以上讨论仅对客户/服务器模式而言,实际上套接字的使用是非常灵活的,唯一需遵循的原则是进程通信之前,必须建立完整的相关。

 

#include<sys/types.h>

#include<sys/socket.h>

int connect(int sockfd,struct sockaddr * servaddr,int addrlen)

参数sockfd是函数socket返回的套接字描述符;参数servaddr指定远程服务器的套接字地址,包括服务器的IP地址和端口号;参数 addrlen指定这个套接字地址的长度。成功时返回0,否则返回-1,并设置全局变量为以下任何一种错误类型:ETIMEOUT、 ECONNREFUSED、EHOSTUNREACH或ENETUNREACH。

在调用函数connect之前,客户机需要指定服务器进程的套接字地址。客户机一般不需要指定自己的套接字地址(IP地址和端口号),系统会自动从 1024至5000的端口号范围内为它选择一个未用的端口号,然后以这个端口号和本机的IP地址填充这个套接字地址。

客户机调用函数connect来主动建立连接。这个函数将启动TCP协议的3次握手过程。在建立连接之后或发生错误时函数返回。连接过程可能出现的错误情况有:

(1)如果客户机TCP协议没有接收到对它的SYN数据段的确认,函数以错误返回,错误类型为ETIMEOUT。通常TCP协议在发送SYN数据段失败之后,会多次发送SYN数据段,在所有的发送都高中失败之后,函数以错误返回。

注:SYN(synchronize)位:请求连接。TCP用这种数据段向对方TCP协议请求建立连接。在这个数据段中,TCP协议将它选择的初始序列号通知对方,并且与对方协议协商最大数据段大小。SYN数据段的序列号为初始序列号,这个SYN数据段能够被确认。当协议接收到对这个数据段的确认之后,建立TCP连接。

(2)如果远程TCP协议返回一个RST数据段,函数立即以错误返回,错误类型为ECONNREFUSED。当远程机器在SYN数据段指定的目的端口号处没有服务进程在等待连接时,远程机器的TCP协议将发送一个RST数据段,向客户机报告这个错误。客户机的TCP协议在接收到RST数据段后不再继续发送SYN数据段,函数立即以错误返回。

注:RST(reset)位:表示请求重置连接。当TCP协议接收到一个不能处理的数据段时,向对方TCP协议发送这种数据段,表示这个数据段所标识的连接出现了某种错误,请求TCP协议将这个连接清除。有3种情况可能导致TCP协议发送RST数据段:(1)SYN数据段指定的目的端口处没有接收进程在等待;(2)TCP协议想放弃一个已经存在的连接;(3)TCP接收到一个数据段,但是这个数据段所标识的连接不存在。接收到RST数据段的TCP协议立即将这条连接非正常地断开,并向应用程序报告错误。

(3)如果客户机的SYN数据段导致某个路由器产生“目的地不可到达”类型的ICMP消息,函数以错误返回,错误类型为EHOSTUNREACH或 ENETUNREACH。通常TCP协议在接收到这个ICMP消息之后,记录这个消息,然后继续几次发送SYN数据段,在所有的发送都告失败之后,TCP 协议检查这个ICMP消息,函数以错误返回。

注:ICMP:Internet 消息控制协议。Internet的运行主要是由Internet的路由器来控制,路由器完成IP数据包的发送和接收,如果发送数据包时发生错误,路由器使用ICMP协议来报告这些错误。ICMP数据包是封装在IP数据包的数据部分中进行传输的,其格式如下:

类型

校验和

数据

0 8 16 24 31

类型:指出ICMP数据包的类型。

代码:提供ICMP数据包的进一步信息。

校验和:提供了对整个ICMP数据包内容的校验和。

ICMP数据包主要有以下类型:

(1)目的地不可到达:A、目的主机未运行;B、目的地址不存在;C、路由表中没有目的地址对应的条目,因而路由器无法找到去往目的主机的路由。

(2)超时:路由器将接收到的IP数据包的生存时间(TTL)域减1,如果这个域的值变为0,路由器丢弃这个IP数据包,并且发送这种ICMP消息。

(3) 参数出错:当IP数据包中有无效域时发送。

(4) 重定向:将一条新的路径通知主机。

(5) ECHO请求、ECHO回答:这两条消息用语测试目的主机是否可以到达。请求者向目的主机发送ECHO请求ICMP数据包,目的主机在接收到这个ICMP 数据包之后,返回ECHO回答ICMP数据包。

(6)时戳请求、时戳回答:ICMP协议使用这两种消息从其他机器处获得其时钟的当前时间。

调用函数connect的过程中,当客户机TCP协议发送了SYN数据段的确认之后,TCP状态由CLOSED状态转为SYN_SENT状态,在接收到对SYN数据段的确认之后,TCP状态转换成ESTABLISHED状态,函数成功返回。如果调用函数connect失败,应该用close关闭这个套接字描述符,不能再次使用这个套接字描述符来调用函数connect。

注:TCP协议状态转换图:

被动OPEN CLOSE 主动OPEN

(建立TCB) (删除TCB) (建立TCB,

发送SYN)

接收SYN SEND

(发送SYN,ACK) (发送SYN)

接收SYN的ACK(无动作)

接收SYN的ACK 接收SYN,ACK

(无动作) (发送ACK)

CLOSE

(发送FIN) CLOSE 接收FIN

(发送FIN) (发送FIN)

接收FIN

接收FIN的ACK(无动作) (发送ACK) CLOSE(发送FIN)

接收FIN 接收FIN的ACK 接收FIN的ACK

(发送ACK) (无动作) (无动作)

2MSL超时(删除TCB)

 

 

 

 

函数accept从征听套接字的完成队列中接收一个已经建立起来的TCP连接。如果完成连接队列为空,那么这个进程睡眠。

#include<sys/socket.h>

int accept(int sockfd,struct sockaddr * addr,int * addrlen)

参数sockfd指定征听套接字描述符;参数addr为指向一个Internet套接字地址结构的指针;参数addrlen为指向一个整型变量的指针。执行成功时,返回3个结果:函数返回值为一个新的套接字描述符,标识这个接收的连接;参数addr指向的结构变量中存储客户机地址;参数 addrlen指向的整型变量中存储客户机地址的长度。失败时返回-1。

征听套接字专为接收客户机连接请求,完成3次握手操作而用的,所以TCP协议不能使用征听套接字描述符来标识这个连接,于是TCP协议创建一个新的套接字来标识这个要接收的连接,并将它的描述符发挥给应用程序。现在有两个套接字,一个是调用函数accept时使用的征听套接字,另一个是函数 accept返回的连接套接字(connected socket)。一个服务器通常只需创建一个征听套接字,在服务器进程的整个活动期间,用它来接收所有客户机的连接请求,在服务器进程终止前关闭这个征听套接字;对于没一个接收的(accepted)连接,TCP协议都创建一个新的连接套接字来标识这个连接,服务器使用这个连接套接字与客户机进行通信操作,当服务器处理完这个客户机请求时,关闭这个连接套接字。

当函数accept阻塞等待已经建立的连接时,如果进程捕获到信号,函数将以错误返回,错误类型为EINTR。对于这种错误,一般重新调用函数 accept来接收连接。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wsguojun2008/archive/2011/02/11/6179037.aspx

 

 

3.4 监听连接──listen() 
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下: 

int PASCAL FAR listen(SOCKET s, int backlog); 

参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。 

listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。 

调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。 

2.3节中提到钥对于客户/服务器模式中,有两种类型的服务:重复服务和并发服务。accept()调用为实现并发服务提供了极大方便,因为它要返回一个新的套接字号,其典型结构为: 

view plaincopy to clipboardprint?
int initsockid, newsockid;    
 
if ((initsockid = socket(....)) < 0)    
 
error(“can’t create socket”);    
 
if (bind(initsockid,....) < 0)    
 
error(“bind error”);    
 
if (listen(initsockid , 5) < 0)    
 
error(“listen error”);    
 
for (; {    
 
newsockid = accept(initsockid, ...) /* 阻塞 */    
 
if (newsockid < 0)    
 
error(“accept error“);    
 
if (fork() == 0){ /* 子进程 */    
 
closesocket(initsockid);    
 
do(newsockid); /* 处理请求 */    
 
exit(0);    
 
}    
 
closesocket(newsockid); /* 父进程 */    
 
}  
int initsockid, newsockid; 

if ((initsockid = socket(....)) < 0) 

error(“can’t create socket”); 

if (bind(initsockid,....) < 0) 

error(“bind error”); 

if (listen(initsockid , 5) < 0) 

error(“listen error”); 

for (; { 

newsockid = accept(initsockid, ...) /* 阻塞 */ 

if (newsockid < 0) 

error(“accept error“); 

if (fork() == 0){ /* 子进程 */ 

closesocket(initsockid); 

do(newsockid); /* 处理请求 */ 

exit(0); 

closesocket(newsockid); /* 父进程 */ 


这段程序执行的结果是newsockid与客户的套接字建立相关,子进程启动后,关闭继承下来的主服务器的initsockid,并利用新的newsockid与客户通信。主服务器的initsockid可继续等待新的客户连接请求。由于在Unix等抢先多任务系统中,在系统调度下,多个进程可以同时进行。因此,使用并发服务器可以使服务器进程在同一时间可以有多个子进程和不同的客户程序连接、通信。对于客户程序看来,服务器可以同时并发地处理多个客户的请求,这就是并发服务器名称的来由。 

面向连接服务器也可以是重复服务器,其结构如下: 

view plaincopy to clipboardprint?
int initsockid, newsockid;    
 
if ((initsockid = socket(....))<0)    
 
error(“can’t create socket”);    
 
if (bind(initsockid,....)<0)    
 
error(“bind error”);    
 
if (listen(initsockid,5)<0)    
 
error(“listen error”);    
 
for (; {    
 
newsockid = accept(initsockid, ...) /* 阻塞 */    
 
if (newsockid < 0)    
 
error(“accept error“);    
 
do(newsockid); /* 处理请求 */    
 
closesocket(newsockid);    
 

int initsockid, newsockid; 

if ((initsockid = socket(....))<0) 

error(“can’t create socket”); 

if (bind(initsockid,....)<0) 

error(“bind error”); 

if (listen(initsockid,5)<0) 

error(“listen error”); 

for (; { 

newsockid = accept(initsockid, ...) /* 阻塞 */ 

if (newsockid < 0) 

error(“accept error“); 

do(newsockid); /* 处理请求 */ 

closesocket(newsockid); 

}

重复服务器在一个时间只能和一个客户程序建立连接,它对多个客户程序的处理是采用循环的方式重复进行,因此叫重复服务器。并发服务器和重复服务器各有利弊:并发服务器可以改善客户程序的响应速度,但它增加了系统调度的开销;重复服务器正好与其相反,因此用户在决定是使用并发服务器还是重复服务器时,要根据应用的实际情考网考网来定。 

 

#include<sys/socket,h>

int listen(int sockfd,int backlog)

参数sockfd指定要转换的套接字描述符;参数backlog设置请求队列的最大长度;执行成功时返回0,否则返回-1。函数listen功能有两个:

(1)将一个尚未连接的主动套接字(函数socket创建的可以用来进行主动连接但不能接受连接请求的套接字)转换成一个被动连接套接字。执行 listen之后,服务器的TCP状态由CLOSED转为LISTEN状态。

(2) TCP协议将到达的连接请求队列,函数listen的第二个参数指定这个队列的最大长度。

注:参数backlog的作用:

TCP协议为每一个征听套接字维护两个队列:

(1)未完成连接队列:每个尚未完成3次握手操作的TCP连接在这个队列中占有一项。TCP希望仪在接收到一个客户机SYN数据段之后,在这个队列中创建一个新条目,然后发送对客户机SYN数据段的确认和自己的SYN数据段(ACK+SYN数据段),等待客户机对自己的SYN数据段的确认。此时,套接字处于SYN_RCVD状态。这个条目将保存在这个队列中,直到客户机返回对SYN数据段的确认或者连接超时。

(2)完成连接队列:每个已经完成3次握手操作,但尚未被应用程序接收(调用函数accept)的TCP连接在这个队列中占有一项。当一个在未完成连接队列中的连接接收到对SYN数据段的确认之后,完成3次握手操作,TCP协议将它从未完成连接队列移到完成连接队列中。此时,套接字处于 ESTABLISHED状态。这个条目将保存在这个队列中,直到应用程序调用函数accept来接收它。

参数backlog指定某个征听套接字的完成连接队列的最大长度,表示这个套接字能够接收的最大数目的未接收连接。如果当一个客户机的SYN数据段到达时,征听套接字的完成队列已经满了,那么TCP协议将忽略这个SYN数据段。对于不能接收的SYN数据段,TCP协议不发送RST数据段,

 

 

 

 

3.5 数据传输──send()与recv() 
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。 

send()调用用于参数s指定的已连接的数据报或流套接字上发送输出数据,格式如下: 

int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags); 

参数s为已连接的本地套接字描述符。buf 指向存有发送数据的缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否发送带外数据等。如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。 

 

recv()调用用于参数s指定的已连接的数据报或流套接字上接收输入数据,格式如下: 

int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags); 

参数s 为已连接的套接字描述符。buf指向接收输入数据缓冲区的指针,其长度由len 指定。flags 指定传输控制方式,如是否接收带外数据等。如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。 

3.6 输入/输出多路复用──select() 
select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下: 

int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR *

exceptfds, const struct timeval FAR * timeout); 

参数nfds指明被检查的套接字描述符的值域,此变量一般被忽略。 

参数readfds指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。参数writefds 指向要做写检测的套接字描述符集合的指针。exceptfds指向要检测是否出错的套接字描述符集合的指针。timeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。 

3.7 关闭套接字──closesocket() 
closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下: 

BOOL PASCAL FAR closesocket(SOCKET s); 

参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。 

 

 

(6) 函数close()

函数close关闭一个套接字描述符。定义如下:

#include<unistd.h>

int close(int sockfd);

执行成功时返回0,否则返回-1。与操作文件描述符的close一样,函数close将套接字描述符的引用计数器减1,如果描述符的引用计数大于 0,则表示还有进程引用这个描述符,函数close正常返回;如果为0,则启动清除套接字描述符的操作,函数close立即正常返回。

调用close之后,进程将不再能够访问这个套接字,但TCP协议将继续使用这个套接字,将尚未发送的数据传递到对方,然后发送FIN数据段,执行关闭操作,一直等到这个TCP连接完全关闭之后,TCP协议才删除该套接字。

(7) 函数read()和write()

用于从套接字读写数据。定义如下:

int read(int fd,char * buf,int len)

int write(int fd,char * buf,int len)

函数执行成功时,返回读或写的数据量的大小,失败时返回-1。

每个TCP套接字都有两个缓冲区:套接字发送缓冲区、套接字接收缓冲区,分别处理发送和接收任务。从网络读、写数据的操作是由TCP协议在内核中完成的:TCP协议将从网络上接收到的数据保存在相应套接字的接收缓冲区中,等待用户调用函数将它们从接收缓冲区拷贝到用户缓冲区;用户将要发送的数据拷贝到相应套接字的发送缓冲区中,然后由TCP协议按照一定的算法处理这些数据。

读写连接套接字的操作与读写文件的操作类似,也可以使用函数read和write。函数read完成将数据从套接字接收缓冲区拷贝到用户缓冲区:当套接字接收缓冲区有数据可读时,1:可读数据量大于函数read指定值,返回函数参数len指定的数据量;2:了度数据量小于函数read指定值,函数 read不等待请求的所有数据都到达,而是立即返回实际读到的数据量;当无数据可读时,函数read将阻塞不返回,等待数据到达。

当TCP协议接收到FIN数据段,相当于给读操作一个文件结束符,此时read函数返回0,并且以后所有在这个套接字上的读操作均返回0,这和普通文件中遇到文件结束符是一样的。

当TCP协议接收到RST数据段,表示连接出现了某种错误,函数read将以错误返回,错误类型为ECONNERESET。并且以后所有在这个套接字上的读操作均返回错误。错误返回时返回值小于0。

函数write完成将数据从用户缓冲区拷贝到套接字发送缓冲区的任务:到套接字发送缓冲区有足够拷贝所有用户数据的空间时,函数write将数据拷贝到这个缓冲区中,并返回老辈的数量大小,如果可用空间小于write参数len指定的大小时,函数write将阻塞不返回,等待缓冲区有足够的空间。

当TCP协议接收到RST数据段(当对方已经关闭了这条连接之后,继续向这个套接字发送数据将导致对方TCP协议返回RST数据段),TCP协议接收到RST数据段时,函数write将以错误返回,错误类型为EINTR。以后可以继续在这个套接字上写数据。

(8) 函数getsockname()和getpeername()

函数getsockname返回套接字的本地地址;函数getpeername返回套接字对应的远程地址。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wsguojun2008/archive/2011/02/11/6179037.aspx

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值