我用夸克网盘分享了「网络编程复习.zip」辛苦整理不易,可以的话给个打赏~
链接:
目录
预备知识
第一章
1.1 网络编程与进程通信
1.1.1 进程与线程的基本概念
1.1.2 因特网中网间进程的标识
1.1.3 网络协议的特征
1.1.4 高效的用户数据报协议UDP
1.1.5 可靠的传输控制协议TCP
1.2 三类网络编程
1.2.1 基于TCP/IP协议栈的网络编程
1.2.2 基于WWW应用的网络编程
1.2.3 基于.NET框架的Web Services网络编程
1.3 客户/服务器交互模式
1.3.1 网络应用软件的地位和功能
1.3.2 客户/服务器模式
1.3.3 客户与服务器的特性
1.3.4 容易混淆的术语
1.3.5 客户与服务器的通信过程
1.3.6 网络协议与C/S模式的关系
1.3.7 错综复杂的客户/服务器交互
1.3.8 服务器如何同时为多个客户服务
1.3.9 标识一个特定服务
第二章
2.1UNX套接字网络编程接口的产生与发展
2.1.1 问题的提出
2.1.2 套接字编程接口的起源与应用
2.1.3 套接字编程接口的两种实现方式
2.1.4 套接字通信与UNIX操作系统的输入/输出
2.2 套接字编程的基本概念
2.2.1 什么是套接字(SOCKET)
2.2.2 套接字的特点
2.2.3 套接字的应用场合
2.2.4 套接字使用的数据类型和相关的问题
1、 3种表示套接字地址的结构
INADDR ANY
2、本机字节顺序和网络字节顺序
3、点分十进制的P地址的转换
4、域名服务
gethostbynamel函数用法
IP 地址转换函数
主机名IP地址函数
2.3 面向连接的套接字编程
2.3.1 套接字的工作过程
2.3.2 UNIX套接字编程接口的系统调用
TCP连接队列和 SOMAXCONN
阻塞式Socket的send函数执行流程
同步Socket的recv函数执行流程
2.3.3 面向连接的套接字编程实例
进程因调用recv()而被阻塞
服务器进程因调用ACCEPT()而被阻塞
代码差错处理
GetLastError()函数的使用
2.3.4 进程的阻塞问题和对策
解除阻塞的方法
select()--多路同步 I/O
使用 struct timeval 和操作 fd_set 集合
2.4 无连接的套接字编程
2.4.1 无连接的套接字编程的两种模式
2.4.2 两个专用的系统调用
2.4.3 数据报套接字的对等模式编程实例
2.5 原始套接字
第三章
3.1 Windows Sockets规范
3.1.1 概述
3.1.2 Windows Sockets规范
阻塞处理例程(阻塞钩子函数) BlockingHook()
3.1.3 WinSock规范与Berkeley套接口的区别
near指针和far指针的区别
3.2 Winsock库函数
3.2.1 Winsock的注册与注销
3.2.2 Winsock的错误处理函数
3.2.3 主要的Winsock函数
3.2.4 Winsock的辅助函数
3.2.5 Winsock的信息查询函数
3.2.6 WSAAsyncGetXByY类型的扩展函数
3.3 Windows环境下的多路异步选择I/O
第四章
4.1 MFC概述
4.1.1 MFC是一个编程框架
4.1.2 典型的MDI应用程序的构成
4.2 MFC和Win32
4.2.1 MFC对象和Windows对象的关系
4.2.2 几个主要的类
4.4 Windows系统的消息机制
4.5 MFC对象的创建
4.6 应用程序的退出
第五章
5.1 CasyncSocket类
5.1.1 使用CAsyncSocket类的一般步骤
5.1.2 创建CAsyncSocket类对象
WM_SOCKET_NOTIFY
5.1.3 关于CAsyncSocket类可以接受并处理的消息事件
5.1.4 客户端套接字对象请求连接到服务器端套接字对象
5.1.5 服务器接受客户端的连接请求
5.1.6 发送与接收流式数据
5.1.7 关闭套接字
5.1.8 错误处理
5.1.9 其它的成员函数
5.2 CSocket类
5.2.1 创建 CSocket 对象
5.2.2 建立连接
5.2.3 发送和接收数据
5.2.4 CSocket类与CArchive类和CSocketFile类
CSocketFile
CArchive 类
5.2.5 关闭套接字和清除相关的对象
5.3 CSocket 类的编程模型
第六章
6.1 WinInet API 的导入
6.1.1 WinINet API 函数使用的 HINTERNET 句柄
6.1.2 典型的操作流程和它们使用的句柄
6.1.3 获取 WinInet API 函数执行的错误信息
6.1.5 WinInet API 的异步操作模式
6.1.6 回调函数的定义实现与注册
6.3 MFC WinInet
6.3.1 概述
MFC WinInet 类的关系
6.3.2 MFC WinInet所包含的类
查询或设置Internet请求选项
CInternetConnection类成员
CFtpConnection
一般使用MFC WinInet的流程:
Internet应用的数据交换流程:
现代软件系统的分布式基础设施通常涉及分布式文件系统、分布式协作系统、分布式数据库系统等支撑系统。分布式基础设施均依赖于计算机网络通信从应用的角度看,计算机网络通信的实现依赖于两个因素:网络通信协议:网络消息传输的基础操作系统网络服务接口(即网络接口):用户利用操作系统提供的API接口实现对网络服务的使用
网络接口层的功能
功能实现层调用网络协议栈的服务实现网络通信的“接口”。
网络通信层需要解决的问题包括:通过什么样的“接口”?如何使用这些“接口”?
本课程的重点(学习如何高效的使用网络接口层)
“接口”是什么?有哪些常用类型?如何定义这些“接口”?
有哪些常用的“接口”使用方式?实际应用中如何选择?如何高效的使用这些接口
系统的各种延迟
1.进程是处于运行过程中的程序实例,是操作系统调度和分配资源的基本单位。
一个进程实体由程序代码、数据和进程控制块三部分构成。
各种计算机应用程序在运行时,都以进程的形式存在。网络应用程序也不例外。
每个进程都有独立的代码和数据空间(进程上下文),进程切换的开销大。
线程:进程中程序代码的一个执行序列。同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
操作系统通常不但支持多进程,还支持多线程。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
2. 网络应用进程在网络体系结构中的位置
从计算机网络体系结构的角度来看,网络应用进程处于网络层次结构的最上层。
从功能上,可以将网络应用程序分为两部分:
一部分是专门负责网络通信的模块,它们与网络协议栈相连接,借助网络协议栈提供的服务完成网络上数据信息的交换。
另一部分是面向用户或者作其他处理的模块,它们接收用户的命令,或者对借助网络传输过来的数据进行加工,这两部分模块相互配合,来实现网络应用程序的功能。
网络应用程序最终要实现网络资源的共享,共享的基础就是必须能够通过网络轻松地传递各种信息。网络编程首先要解决网间进程通信的问题。然后才能在通信的基础上开发各种应用功能。
3. 实现网间进程通信必须解决的问题
网间进程通信是指网络中不同主机中的应用进程之间的相互通信问题,网间进程通信必须解决以下问题:
(1)网间进程的标识问题(2)如何调用网络协议栈服务(3)多重应用协议的识别问题(4)不同的通信服务质量的问题
1.传输层在网络通信中的地位
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信的能力。TCP/IP协议提出了传输层协议端口(protocol port,简称端口)的概念,成功地解决了通信进程的标识问题。
传输层是计算机网络中,通信主机内部进行独立操作的第一层,是支持端到端的进程通信的关键的一层。
2.端口的概念
端口是TCP/IP协议族中,应用层进程与传输层协议实体间的通信接口
类似于进程ID号,每个端口都拥有一个叫作端口号(port number)的整数型标识符,端口号唯一标识了本机网络协议栈上的一个通信接口。
从实现的角度讲,端口是一种抽象的软件机制,包括一些数据结构和I/O缓冲区。
3.端口号的分配机制
TCP/IP协议采用了全局分配(静态分配)和本地分配(动态分配)相结合的分配方法。对于TCP,或者UDP,将它们的全部65535个端口号分为保留端口号和自由端口号两部分。
保留端口的范围是0—1023,又称为众所周知的端口或熟知端口(well-known port),只占少数,采用全局分配或集中控制的方式,由一个公认的中央机构根据需要进行统一分配,静态地分配给因特网上著名的众所周知的服务器进程,并将结果公布于众。
其余的端口号,1024-65535,称为自由端口号,采用本地分配,又称为动态分配的方法。
总之,TCP或UDP端口的分配规则是:
端口0:不使用,或者作为特殊的使用;
端口1-255:保留给特定的服务,TCP和UDP均规定,小于256的端口号才能分配给网上著名的服务;
端口256-1023:保留给其他的服务,如路由;
端口1024-4999:可以用作任意客户的端口;
端口5000-65535:可以用作用户的服务器端口。
4.进程的网络地址的概念
在因特网络中,用一个三元组可以在全局中唯一地标识一个应用层进程:
应用层进程标识=(传输层协议,主机的IP地址,传输层的端口号)
这样一个三元组,叫做一个半相关(half-association),它标识了因特网中,进程间通信的一个端点,也把它称为进程的网络地址。
5.网络中进程通信的标识
一个完整的网间通信需要一个五元组在全局中唯一地来标识:
(传输层协议,本地机IP地址,本地机传输层端口,远地机IP地址,远地机 传输层端口)
这个五元组称为一个全相关(association)。即两个协议相同的半相关才能组合成一个合适的全相关,或完全指定一对网间通信的进程。
1.面向消息的协议与基于流的协议
(1)面向消息的协议:面向消息的协议以消息为单位在网上传送数据,在发送端,消息一条一条地发送,在接收端,也只能一条一条地接收,每一条消息是独立的,消息之间存在着边界。
(2)基于流的协议:基于流的协议不保护消息边界,将数据当作字节流连续地传输,不管实际消息边界是否存在。
2.面向连接的服务和无连接的服务
一个协议可以提供面向连接的服务,或者提供无连接的服务。
面向连接服务是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连接的过程。
无连接服务是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。
3.可靠性和次序性
次序性是指对数据到达接收端的顺序进行处理。保护次序性的协议保证接收端收到数据的顺序就是数据的发送顺序,称为按序递交。
可靠性保证了发送端发出的每个字节都能到达既定的接收端,不出错,不丢失,不重复,保证数据的完整性,称为保证投递。
次序性的保障机制是实现可靠性的前提条件。
传输层的用户数据报协议(User Datagram Protocol,UDP)是一种尽力传送的无连接的不保障可靠的传输服务,是一种保护消息边界的数据的传输。
1.可靠性是很多应用的基础
2.TCP为应用层提供的服务
传输控制协议 (Transmission Control Protocol,TCP)应用层进程提供一个面向连接的、端到端的、完全可靠的(无差错、无丢失、无重复或失序)全双工的流传输服务。
3.TCP利用IP数据报实现了端对端的传输服务
TCP被称作一种端对端(end to end)协议,这是因为它提供一个直接从一台计算机上的应用进程到另一远程计算机上的应用进程的连接。
应用进程能请求TCP构造一个连接,通过这个连接发送和接收数据,以及关闭连接。
由TCP提供的连接叫做虚连接(virtual connection),虚连接是由软件实现的。
4.三次握手
为确保连接的建立和终止都是可靠的,TCP使用三次握手(3-way handshake)的方式来建立连接
至此,整个连接已经全部释放。
从 A 到 B 的连接就释放了,连接处于半关闭状态。相当于 A 向 B 说:“我已经没有数据要发送了。但你如果还发送数据,我仍接收。”
基于TCP/IP协议栈的网络编程是最基本的网络编程方式,主要是使用各种编程语言,利用操作系统提供的套接字网络编程接口,直接开发各种网络应用程序。本书主要讲解这种网络编程的相关技术。
WWW应用(Web应用)是因特网上最广泛的应用。基于WWW应用的网络编程技术,包括所见即所得的网页制作工具,和动态服务器页面的制作技术。
1.关于.NET平台
Web服务从由简单网页构成的静态服务网站,发展到可以交互执行一些复杂步骤的动态服务网站,这些服务可能需要一个Web服务调用其他的Web服务,并且像一个传统软件程序那样执行命令。这就需要和其他服务整合,例如:**需要多个服务能够一起无缝地协同工作,需要能够创建出与设备无关的应用程序,需要能够容易地协调网络上的各个服务的操作步骤,容易地创建新的用户化的服务。**
微软公司推出的.NET系统技术正是为了满足这种需求。微软公司在2000年7月公布了.NET平台开发框架,.NET将Internet本身作为构建新一代操作系统的基础,并对Internet和操作系统的设计思想进行了延伸,使开发人员能够创建出与设备无关的应用程序,容易地实现Internet连接。
.NET开发平台是一组用于建立Web服务器应用程序和Windows桌面应用程序的软件组件(综合类库),用该平台创建的应用程序在Common Language Runtime(CLR)(通用语言运行环境)(底层)的控制下运行。
综合类库提供了使应用程序可以读写XML数据、在Internet上通信、访问数据库等的代码。所有的类库都建立在一个基础的类库之上,它提供管理使用最为频繁的数据类型(例如数值或文本字符串)的功能,以及诸如文件输入/输出等底层功能。
CLR是一个软件引擎,用来加载应用程序,确认它们可以没有错误地执行,进行相应的安全许可验证,执行应用程序,然后在运行完成后将它们清除。
2.关于Web服务
什么是Web服务?Web服务是松散耦合的可复用的软件模块,在Internet上发布后,能通过标准的Internet 协议在程序中访问,具有以下的特点:
(1)可复用 (2)松散耦合 (3)封装了离散(4)Web服务可以在程序中访问(5)Web服务在Internet上发布
Web 服务是一种可以用来解决跨网络应用集成问题的开发模式,这种模式为实现“软件作为服务”提供了技术保障。
“软件作为服务”实质上是一种提供软件服务的机制,这种机制可以在网络上暴露可编程接口,并通过这些接口来共享站点开放出来的功能。
从技术角度来讲,Web 服务实现了最广泛的应用软件集成,弥补了传统分布式软件开发模型的限制。
传统的分布式软件开发模型各自为政,所以只能用来开发紧耦合类型的分布式应用系统。所谓紧耦合,就是指客户端必须按照特定的规范去访问服务端提供的服务,而这种规范只在一个有限的范围内通用。
为了可以在整个因特网中实现对服务的自由访问,有必要提供一种崭新的模式或信息交换手段来达到这个目的。于是,微软提出了Web 服务。
Web 服务的主要特点之一是,客户端访问Web 服务只需要通过因特网标准协议,如HTTP或XML,以及SOAP,不需要专门的协议。因为HTTP协议和XML都是与平台无关的标准协议,因此,可以被任何主流操作系统正确理解和解释。
另外,更为关键的特性是,Web 服务可以被XML语言进行详尽的描述。这就是说,提供Web服务的站点可以提供一个(或多个)该站点可以对外提供服务的描述文件,这个文件的内容可以被访问者理解。更进一步说,就是客户端可以从网络上直接得到代码!
假设开发人员需要搭建一个商务网站,这个网站需要一个验证客户合法身份的功能。为了实现这个功能,下面分别描述了可以采用的办法。
● 由开发人员自己编写安全验证所需的全部代码。这样做显然不现实,一个安全验证程序涉及到诸多专业知识,并需要相当长的时间才能够完成。
● 购买这段程序(通常是一个ActiveX组件)。在收到组件之后,首先将组件注册在自己的机器上,然后根据组件类型库产生接口文件。在实际编程中就可以使用这个接口文件来访问组件服务。很明显,这种方式在目前使用得最为广泛。
● 有了Web 服务,情况就不同了,只需要在自己的程序中通过访问某个服务的URL地址,得到一份XML描述,并使用这个描述文件产生一个接口文件。然后,在实际编程中,只需要通过这个接口文件来访问服务就可以了。一定要注意,这个服务可不是运行在我们机器上的,是运行在因特网上URL地址所指向的地方。
如果这个网站需要更多的功能,而这些功能在一些网站上已经被开发出来,并以各种方式(免费或收费)公开出来供所有需要它们的开发人员来使用,那么,尽量使用它们好了。当然,如果开发人员所在的公司,也想成为Web 服务提供者的话,同样可以轻松地将他们编写的Web 服务在网络上公布出来,供大家使用。
与紧耦合服务概念相对,由于Web 服务具备通信协议标准性和服务自描述性,所以,使用Web 服务可以开发出松耦合的分布式应用程序来。这也是Web 服务要实现的最根本的设计目标。Web 服务的体系如图所示。
Internet仅仅提供一个通用的通信构架,它只负责传送信息,而对于信息传过去干什么用,利用因特网究竟提供什么服务,由哪些计算机来运行这些服务,如何确定服务的存在,如何使用这些服务等等问题,都要由应用软件和用户解决。
网络应用进程通信时,普遍采用客户/服务器交互模式(client-server paradigm of interaction),简称C/S模式。这是因特网上应用程序最常用的通信模式,即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。
C/S模式的建立基于以下两点:
网络中软硬件资源、运算能力和信息分布的不对等; 网间进程通信完全是异步的,需要一种机制为通信的进程之间建立联系,为二者的数据交换提供同步。
C/S模式过程中服务器处于被动服务的地位。首先服务器方要先启动,并根据客户请求提供相应服务,服务器的工作过程是:
(1)打开一通信通道,并告知服务器所在的主机,它愿意在某一公认的地址上(熟知知端口,如FTP为21)接收客户请求。(2)等待客户的请求到达该端口。
(3)服务器接收到服务请求,处理该请求并发送应答信号。为了能并发地接收多个客户的服务请求,要激活一个新进程或新线程来处理这个客户请求(如UNIX系统中用fork、exec)。服务完成后,关闭此新进程与客户的通信链路,并终止。
(4)返回第二步,等待并处理另一客户请求。(5)在特定的情况下,关闭服务器。
客户方采取的是主动请求方式,其工作过程是:
(1)打开一通信通道,并连接到服务器所在主机的特定监听端口。(2)向服务器发送请求报文,等待并接收应答;继续提出请求,与服务器的会话按照应用协议进行。(3)请求结束后,关闭通信通道并终止。
从上面的描述可知:
客户机和服务器都是运行于计算机中的网络协议栈之上的应用进程,借助网络协议栈进行通信。
服务器运行于高档的服务器类计算机之上,借助网络,可以为成千上万的客户机服务。
客户机软件运行在用户的PC上,有良好的人机界面,通过网络请求并得到服务器的服务,共享网络信息和资源。
客户软件和服务器软件通常还具有以下一些主要特点:
1.客户软件
(1)在进行网络通信时临时成为客户,但它也可在本地进行其他的计算。
(2)被用户调用,只为一个会话运行。在打算通信时主动向远地服务器发起通信。
(3)能访问所需的多种服务,但在某一时刻只能与一个远程服务器进行主动通信。
(4)主动地启动与服务器的通信。
(5)在用户的计算机上运行,不需要特殊的硬件和很复杂的操作系统。
2.服务器软件
(1)是一种专门用来提供某种服务的程序,可同时处理多个远地客户的请求。
(2)当系统启动时即自动调用,并且连续运行着,不断地为多个会话服务。
(3)接受来自任何客户的通信请求,但只提供一种服务。
(4)被动地等待并接受来自多个远端客户的通信请求。
(5)在共享计算机上运行,一般需要强大的硬件和高级的操作系统支持。
3.基于因特网的C/S模式的应用程序的特点
(1)客户和服务器都是软件进程,C/S模式是网络上通过进程通信建立分布式应用的常用模型。
(2)非对称性:服务器通过网络提供服务,客户通过网络使用服务,这种不对称性体现在软件结构和工作过程上。
(3)对等性:客户和服务器必有一套共识的约定,必与以某种应用层协议相联,并且协议必须在通信的两端实现。比如浏览器和3W服务器就都基于HTTP超文本传输协议。
(4)服务器的被动性:服务器必须先行启动,时刻监听,日夜值守,及时服务,只要有客户请求,就立即处理并响应,回传信息。但决不主动提供服务。
(5)客户机的主动性:客户机可以随时提出请求,通过网络得到服务,也可以关机走人,一次请求与服务的过程是由客户机首先激发的。
(6)一对多:一个服务器可以为多个客户机服务,客户机也可以打开多个窗口,连接多个服务器。
(7)分布性与共享性:资源在服务器端组织与存储,通过网络分散在多个客户端使用。
C/S模式优缺点
优点:
结构简单,系统中不同类型的任务分别由客户和服务器承担,有利于发挥不同机器平台的优势;
支持分布式、并发环境,可以有效地提高资源的利用率和共享程度;
服务器集中管理资源,有利于权限控制和系统安全。
由于客户端实现与服务器的直接相连,没有中间环节,因此响应速度快。
操作界面漂亮、形式多样,可以充分满足客户自身的个性化要求。
缺点:
需要专门的客户端安装程序,分布功能弱,针对点多面广且不具备网络条件的用户群体,不能够实现快速部署安装和配置。
兼容性差,对于不同的开发工具,具有较大的局限性。若采用不同工具,需要重新改写程序。
开发成本较高,需要具有一定专业水准的技术人员才能完成。
1.服务器程序与服务器类计算机
服务器(server)这个术语来指那些运行着的服务程序。
服务器类计算机(server-class computer)这一术语来称呼那些运行服务器软件的强大的计算机。
2.客户与用户
“客户”(client)和服务器都指的是应用进程,即计算机软件。
“用户”(user)指的是使用计算机的人。
客户与服务器的通信过程一般是这样的:
(1)通信之前,服务器应先行启动,并通知它的下层协议栈做好接收客户请求的准备,然后被动地等待客户的通信请求,称服务器处于监听状态。
(2)一般是先由客户向服务器发送请求,服务器向客户返回应答。客户随时可以主动启动通信,向服务器发出连接请求,服务器接收这个请求,建立了二者的通信关系。
(3)客户与服务器的通信关系一旦建立,客户和服务器都可发送和接收信息。信息在客户与服务器之间可以沿任一方向或两个方向传递。在某些情况下,客户向服务器发送一系列请求,服务器相应地返回一系列应答。
客户与服务器作为两个软件实体,它们之间的通信是虚拟的,是概念上的,实际的通信要借助下层的网络协议栈来进行。
网络应用进程与应用层协议的关系:
为了具体的应用问题而彼此通信的进程称为应用进程
应用层协议并不解决用户的各种具体问题,而是规定了应用进程在通信时所必须遵循的约定
从网络体系结构的角度来说,应用层协议位于应用进程之下,应用层协议是为应用进程提供服务的,帮助应用进程组织数据。
应用层协议往往在应用进程中实现
在C/S模式中,存在着三种一个与多个的关系:
(1)一个服务器同时为多个客户服务;
(2)一个用户的计算机上同时运行多个连接不同服务器的客户
(3)一个服务器类的计算机同时运行多个服务器
并发性是客户/服务器交互模式的基础,并发允许多个客户获得同一种服务,而不必等待服务器完成对上一个请求的处理。这样才能很好地同时为多个客户提供服务。
在一台服务器类的计算机中可以并发地运行多个服务器进程。它们都要借助协议栈来交换信息,协议栈就是多个服务器进程传输数据的公用通道
这有了一个问题,既然在一个服务器类计算机中运行着多个服务器,如何能让客户无二义性地指明所希望的服务?
这个问题是由传输协议栈提供的一套机制来解决的。这种机制必须赋给每个服务一个唯一的标识,并要求服务器和客户都使用这个标识。
当服务器开始执行时,它在本地的协议栈软件中登记,指明它所提供的服务的标识。当客户与远程服务器通信时,客户在提出请求时,通过这个标识来指定所希望的服务。
客户端机器的传输协议栈软件在发送请求之前也会给客户端进程分配一个唯一的标识。
客户端机器的传输协议栈软件将服务器标识和客户端标识同时传给服务器端机器。服务器端机器的传输协议栈则根据该标识对来确定当前的服务会话过程。
从应用程序实现的角度,应用程序如何方便地使用协议栈软件进行通信呢?
如果能在应用程序与协议栈软件之间提供一个软件接口,就可以方便客户与服务器软件的编程。
套接字应用程序编程接口是网络应用程序通过网络协议栈进行通信时所使用的接口,即应用程序与协议栈软件之间的接口,简称套接字编程接口(Socket API)。
它定义了应用程序与协议栈软件进行交互时可以使用的一组操作,决定了应用程序使用协议栈的方式、应用程序所能实现的功能、以及开发具有这些功能的程序的难度。
加州大学伯克利(Berkley)分校开发并推广了一个包括TCP/IP互联协议的UNIX,称为BSD UNIX(Berkeley Software Distribution UNIX)操作系统,套接字编程接口是这个操作系统的一个部分。
后来的许多操作系统并没有另外搞一套其它的编程接口,而是选择了对于套接字编程接口的支持。
由于这个套接字规范最早是由Berkeley大学开发的,一般将它称为Berkeley Sockets规范。
要想实现套接字编程接口,可以采用两种实现方式:
一种是在操作系统的内核中增加相应的软件来实现,
一种是通过开发操作系统之外的函数库来实现。
UNIX操作系统对文件和所有其它的输入/输出设备采用一种统一的的操作模式,就是“打开-读-写-关闭”(open-read-write-close)的I/O模式。
用户进程I/O操作的基本流程:
调用open命令,获得对指定文件或设备的使用权,并返回一个描述符(描述符是用来标识该文件或设备的整型数,作为用户在打开的文件或设备上进行操作的句柄)
然后这个用户进程可以多次调用“读”或“写”命令来传输数据。在读写命令中,要将描述符作为命令的参数,来指明所操作的对象。
当所有传输操作完成后,用户进程调用close命令,通知操作系统已经完成了对某对象的使用,释放所占用的资源。
当TCP/IP协议被集成到UNIX内核中的时候,相当于在UNIX系统中引入了一种新型的I/O操作,就是应用程序通过网络协议栈来交换数据。
在UNIX系统的实现中,套接字是完全与其他/O集成在一起的。操作系统和应用程序都将套接字编程接口看作一种输入/输出机制。
·操作过程类似。沿用打开-读-写-关闭模式
·操作方法类似。采用套接字描述符。
·使用的过程的名字可以是相同的。
用户进程与网络协议的交互作用实际要比用户进程与传统的I/O设备相互作用要复杂得多。
使用套接字的应用程序必须说明很多细节。仅仅提供open,read,write,close四个过程远远不够。为避免单个套接字函数参数过多,套接字编程接口的设计者定义了多个函数。
套接口是对网络中不同主机上应用进程之间进行双向通信的端点的抽象,一个套接口就是网络上进程通信的一端,提供了应用层进程利用网络协议栈交换数据的机制。
应当从多个层面来理解套接字这个概念的内涵:
·从套接字所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议栈进行通信的接口,是应用程序与网络协议栈进行交互的接口。
·从实现的角度来讲,非常复杂。套接字是一种复杂的软件机制,是一种包含了特定的数据结构,包含许多选项,由操作系统内核管理的内核对象。
·从使用的角度来讲,对于套接字的操作形成了一种网络应用程序的编程接口(API),包括一组操作套接字的系统调用,或者是库函数,把这一组操作套接字的编程接口函数称作套接字编程接口,套接字是它的操作对象。总之,套接字是网络通信的基石
1、通信域
套接字存在于通信域中,通信域是为了处理一般的进程通过套接字通信而引入的一种抽象概念,套接字通常只和同一域中的套接字交换数据。
如果数据交换要穿越域的边界,就一定要执行某种解释程序。
现在,仅仅针对Internet.域,并且使用Internet协议族(即TCP/IP协议族)来通信。
套接字实际是通过网络协议栈来进行通信,是对网络协议栈通信服务功能的抽象和封装,通信双方应当使用相同的通信协议。
通信域是一个计算机网络的范围,在这个范围中,所有的计算机使用同一种网络体系机构,使用同一种协议栈。
2、套接字具有三种类型
每一个正被使用的套接字都有它确定的类型,只有相同类型的套接字才能相互通信。
(1)数据报套接字(Datagram SOCKET)
数据报套接字提供无连接的、不保证可靠的、独立的数据报传输服务。在Internet通信域中,数据报套接字使用UDP数据报协议形成的进程间通路,具有UDP协议为上层所提供的服务的所有特点。
(2)流式套接字(Stream SOCKET)
流式套接字提供双向的、有序的、无重复的、无记录边界的可靠的数据流传输服务。在Internet通信域中,流式套接字使用TCP协议形成的进程间通路,具有TCP协议为上层所提供的服务的所有特点,在使用流式套接字传输数据之前,必须在数据的发送端和接收端之间建立连接。
(3)原始式套接字(RAW SOCKET)
·原始式套接字允许对较低层次的协议,如P、ICMP直接访问,用于检验新的协议的实现。
·原始式套接字可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,可以通过原始式套接字来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于原始式套接字。
3、套接字由应用层的通信进程创建,并为其服务
套接字是一种内核对象,由操作系统内核进行直接管理;
套接字需由用户进程进行创建,创建成功即表示获得了对系统网络协议栈的1次使用权;
用户进程在需要进行网络通信时,通过套接字间接的使用系统网络协议栈资源;
实际的网络通信过程,由操作系统内核根据用户的要求进行直接管理和调度;
4、使用确定的IP地址和传输层端口号
用户进程创建套接字成功后,会得到一个套接字描述符(一种资源句柄)
在得到套接字描述符后,要将套接字与计算机上的特定的P地址和传输层端口号相关联,这个过程称为绑定(使当前套接字与其他用户的套接字相区分)
一个套接字要使用一个确定的三元组网络地址信息,才能使它在系统中唯一地被标识。
套接字编程适合于开发一些新的网络应用,这类应用具有如下特点:
(1)不管是采用对等模式或者客户机/服务器模式,通信双方的应用程序都需要开发。
(2)双方所交换数据的结构和交换数据的顺序有特定的要求,不符合现在成熟的应用层协议,甚至需要自己去开发应用层协议,自己设计最适合的数据结构和信息交换规程。
在套接字编程接口中,专门定义了三种结构体数据类型,用来存储协议相关的网络地址,在套接字编程接口的函数调用中要用到它们。
(1)通用的socket地址:sockaddr结构,针对各种通信域的套接字,存储它们的地址信息。
struct sockaddr{unsigned short sa family; //地址家族char sa data[14]; //14字节协议地址}
(2)sockaddr_in结构,专门针对Internet通信域,存储套接字相关的网络地址信息,例如IP地址,传输层端口号等信息。
struct sockaddr_in{short int sin_family; //地址家族unsigned short int sin_port; //端口号struct in_addr sin_addr; //IP地址unsigned char sin_zero[8]; //全为0}
(3)in_addr结构,专门用来存储IP地址。
struct in_addr{union{struct{u_char s_b1,s_b2,s_b3,s_b4;}S_un b;struct {u_short s_w1,s_w2;}S_un_w;u long S_addr;}S_un;#define s_addr S_un.S_addr;};
注意:IP地址以网络字节序进行保存
The IN ADDR derived structures are only defined on the Windows SDK released with Windows Vista and later. On earlier versions of the Windows SDK,variables of this type should be declared as struct in_addr.
例:IP地址10.14.25.90,依据in addr结构体的定义,可以有4种不同的表示方式:假设定义结构体变量in_addr sin_addr
1、 sin_addr.S_un.S_un.b.s_b1=10;
sin_addr.S_un.S_un.b.s_b2=14;
sin_addr.S_un.S_un.b.s_b3=25;
sin_addr.S_un.S_un.b.s_b4=90;
2、 sin_addr.S_un.S_un_w.s_w1=(14<<8)10;
sin_addr.S_un.S_un_w.s_w1=(90<<8)|25;
3、 sin_addr.S_un.S_addr=(90<<24)l(25<<16)l(14<<8)|10;
4、 sin_addr.s_addr=(90<<8)(25<<16)|(14<<8)|10;
IPv6的地址结构体
struct in6_addr {u_int8_t s6_addr[16];/*128bit字节地址*/};#define SIN6 LEN /*required for compile-time tests */struct sockaddr_in6 {u_int8_t sin6_len;/*SIN6 LEN */sa_family_t sin6_family;/*AF INET6 */in_port_t sin6_port;u_int32_t sin6_flowinfo;/*priority and flow label */struct in6_addr sin6_addr;/*IlPV6的任意地址是in6addr_any*/u_int32_t sin6_scope_id;};
(4)这些数据结构的一般用法:
首先,定义一个Sockaddr_in
的结构实例,并将它清零。比如:
struct sockaddr_in myad;memset(&myad,O,sizeof(struct sockaddr in));
函数原型:void * memset(void *s,int ch,unsigned n);
然后,为这个结构赋值,如:
myad.sin_family=AF_INET;myad.sin_port=htons(8080);myad.sin_addr.s_addr=INADDR-ANY;
最后,在函数调用中使用时,将这个结构强制转换为sockaddr类型。
accept(listenfd,(sockaddr*)(&myad),&addrlen);
表示不确定地址,或“所有地址”、“任意地址”。一般来说,在各个系统中均定义成为0值。 将IP地址指定为INADDR_ANY,允许套接字向任何分配给本地机器的P地址发送或接收数据。 多数情况下,每个机器只有一个P地址,但有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR ANY可以简化应用程序的编写。将地址指定为NADDR ANY,允许一个独立应用接受发自多个接口的回应。 如果我们只想让套接字使用多个P中的一个地址,就必须指定实际地址。
·在具体计算机中的多字节数据的存储顺序,称为本机字节顺序。不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存)。基于Intel的CPU,即我们常用的PC机采用的是低位先存。
·多字节数据在网络协议报头中的存储顺序,称为网络字节顺序。为保证数据的正确性,在网络协议中需要指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高位先存格式。
网络应用程序要在不同的计算机中运行,本机字节顺序是不同的,但是,网络字节顺序是一致的。
所以,应用程序在编程的时候,在把P地址和端口号装入套接字的时候,应当把它们从本机字节顺序转换为网络字节顺序;相反,在本机输出时,应将它们从网络字节顺序转换为本机字节顺序。
套接字编程接口特为解决这个问题设置了四个函数:
htons():短整数本机顺序转换为网络顺序,用于端口号。
htonl():长整数本机顺序转换为网络顺序,用于IP地址。
ntohs():短整数网络顺序转换为本机顺序,用于端口号。
ntohl():长整数网络顺序转化为本机顺序,用于IP地址。
这四个函数将被转换的数值作为函数的参数,函数返回值是转换后的结果。
在因特网中,IP地址常常用点分十进制的表示方法,但在套接字中,IP地址是无符号的长整型数,套接字编程接口设置了两个函数,专门用于两种形式的IP地址的转换。
(1)inet_addr函数:
unsigned long inet-addr(const char*cp)
入口参数cp:点分十进制形式的P地址。
返回值:网络字节顺序的P地址,是无符号的长整数。
const int *ptr; 表示ptr所指的地址可更改,但ptr指向的数据内容不可更改。
int *const ptr; 表示ptr所指的地址不可更改,但ptr指向的数据内容可更改。
const int *const ptr; 表示pr所指的地址和ptr指向的数据内容均不可更改。
(2)inet ntoal函数:
char*inet ntoa(struct in_addr in)
入口参数in:包含长整型IP地址的in_addr结构变量
返回值:指向点分十进制P地址的字符串的指针。
注意的是:函数inet ntoa0的参数是struct in addr,而不是Iong。同时要注意的是该函数返回值为指向字符串地址的指针,该字符串的空间为静态分配的,这意味着在第二次调用该函数时,上一次调用将会被重写(复盖)。例如:
char *al,*a2; a1 = inet_ntoa(inal.sin_addr);/*this is 198.92.129.1 */ a2 = inet_ntoa(ina2.sin_addr);/*this is 132.241.5.10*/ printf("address 1:%s n",al); printf("address 2:%s n",a2);
运行结果是:
address 1:132.241.5.10 address 2:132.241.5.10
如果你想保存地址,那么用strcpy() 保存到自己的字符数组中。
通常,我们使用域名来标识站点,可以将文字型的主机域名直接转换成IP地址,
struct hostent*gethostbyname(const char* name);
入口参数:是站点的主机域名字符串
返回值:是指向hostent结构的指针,
hostent结构包含主机名,主机别名数组,返回地址的类型(一般是AF_INET),地址长度的字节数,已符合网络字节顺序的主机网络地址等。
#include <netdb.h> struct hostent{char *h_name;char **h_aliases;int h_addrtype;int h_length;char **h_addr_list;}; #define h_addr h_addr_list[O]
这个结构的解释:
char *h_name
表示的是主机的规范名。例如le的规范名其实是le.
char **h aliases
表示的是主机的别名。
int h_addrtype
表示的是主机ip地址的类型,ipv4(AF_INET) , ipv6(AF_INET6)
int h_length
表示的是主机ip地址的长度
char **h_addr_list
表示的是主机的ip地址,注意,这个是以网络字节序存储的。
例子:
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/socket.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h>int main(int argc, char *argv[]) {struct hostent *h;if (argc != 2) {// 错误检查命令行参数fprintf(stderr, "usage: get ip addressn");exit(1);}if ((h = gethostbyname(argv[1])) == NULL) {// 获取主机信息herror("gethostbyname");exit(1);}printf("Host name: %sn", h->h_name);printf("IP Address: %sn", inet_ntoa(*((struct in_addr *)h->h_addr)));return 0; }
#include <stdio.h> #include <netdb.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h>int main(int argc, char **argv) {char *ptr, **pptr;struct hostent *hptr;char str[32];/*取得命令后第一个参数,即要解析的域名或主机名*/ptr = argv[1];/* 调用gethostbyname()获取主机信息 */if ((hptr = gethostbyname(ptr)) == NULL) {printf("gethostbyname error for host: %sn", ptr);return 1; /* 如果调用gethostbyname发生错误,返回1 */}/* 打印规范主机名 */printf("Official hostname: %sn", hptr->h_name);/* 主机可能有多个别名,将所有别名分别打印出来 */for (pptr = hptr->h_aliases; *pptr != NULL; pptr++) {printf("Alias: %sn", *pptr);}/* 根据地址类型,将地址打印出来 */switch (hptr->h_addrtype) {case AF_INET:case AF_INET6:pptr = hptr->h_addr_list;/* 将刚才得到的所有地址都打印出来。其中调用了inet_ntop()函数 */for (; *pptr != NULL; pptr++) {printf("Address: %sn", inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));}break;default:printf("Unknown address typen");break;}return 0; }
这段代码是一个简单的C程序,用于获取主机名对应的信息,包括官方主机名、别名以及对应的IP地址列表,并根据地址类型打印IP地址。以下是对代码的详细解释:包含所需的头文件。
<stdio.h>
:标准输入输出库,用于打印信息到控制台。
<netdb.h>
:网络数据库操作库,包含了主机信息查询函数。
<arpa/inet.h>
:包含了用于IP地址转换的函数,如inet_ntop
。
<netinet/in.h>
:包含了与网络相关的数据结构和宏定义。
<sys/socket.h>
:包含了套接字编程相关的函数和数据结构。
main
函数是程序的入口函数,接受命令行参数argc
和argv
。
ptr
是一个字符指针,用于存储命令行参数中传递的主机名或域名。使用
gethostbyname()
函数获取主机信息,并将结果保存在hptr
中。如果获取失败,程序会打印错误信息并返回1。打印官方主机名
hptr->h_name
。使用
for
循环遍历可能的别名列表,并打印出每个别名。使用
switch
语句根据地址类型来处理IP地址。如果是IPv4或IPv6,将遍历地址列表,并使用inet_ntop()
函数将二进制IP地址转换为可读的字符串形式,并打印出来。如果地址类型不是IPv4或IPv6,则打印"Unknown address type"。
返回0表示成功执行,程序正常结束。
这个程序的主要目的是获取一个主机的信息,包括其官方主机名、别名和IP地址。它演示了如何使用一些网络编程的基本函数来获取这些信息,并将其打印到控制台。需要注意的是,
gethostbyname()
在现代网络编程中已经不再推荐使用,建议使用getaddrinfo()
函数来代替,因为后者支持IPv4和IPv6,并提供更多灵活的选项。
输出:
对于 bit.edu
:
Official hostname: bit.edu Address: 202.204.80.7
对于 xmu.edu
:
Official hostname: xmu.edu Address: 210.34.0.2
对于 sina
:
Official hostname: sina Address: 71.5.7.191
对于 sohu
:
Official hostname: sohu Address: 61.135.181.176 Address: 61.135.181.175
在Linux下的 inet_pton
和 inet_ntop
这两个 IP 地址转换函数,可以在将 IP 地址在 "点分十进制" 和 "整数" 之间进行转换。而且,这两个函数能够处理 IPv4 和 IPv6。其中,
const char *inet_ntop(int af, const void *src, char *dst, socklen_t cnt);
参数:
af
:地址族,可以是 AF_INET
(IPv4)或 AF_INET6
(IPv6)。
src
:要转换的点分十进制字符串表示的 IP 地址。
dst
:存储转换后的二进制 IP 地址的目标缓冲区。
socklen_t cnt
:套接字长度
用于将二进制 IP 地址转换为点分十进制字符串表示。
在Windows中也提供了对应的两个函数,InetPton
和 InetNtop
(VS2008以上可用)。其中,
PCTSTR WSAAPI InetNtop(_In_ INT Family, _In_ PVOID pAddr, _Out_ PTSTR pStringBuf, _In_ size_t StringBufSize);
用于将二进制 IP 地址转换为点分十进制字符串表示。
gethostname
函数返回运行程序的计算机的主机名。然后,你可以使用 gethostbyname()
函数来获取该计算机的IP地址。以下是函数的定义:
#include <unistd.h> int gethostname(char *hostname, size_t size);
参数非常简单:hostname
是一个字符数组指针,它将在函数返回时存储主机名,size
是 hostname
数组的字节长度。
函数成功调用时返回 0,失败时返回 -1。
gethostbyname
函数:
功能:根据主机名获取主机的IP地址。
原型:struct hostent *gethostbyname(const char *name);
参数:
name
:要查询的主机名的字符串。
返回值:
如果查询成功,返回一个指向 struct hostent
结构的指针,该结构包含了主机名、别名和IP地址列表等信息。
如果查询失败,返回NULL
。
使用示例:
#include <netdb.h> struct hostent *host; host = gethostbyname("example"); if (host != NULL) {printf("Official hostname: %sn", host->h_name);// 打印别名和IP地址列表等信息 } else {herror("gethostbyname"); }
这个函数用于根据主机名查询主机的IP地址和相关信息,以便在网络编程中建立连接或进行通信。
p33 图2.5
1、创建套接字SOCKET()---打开一个通道(第1步)
socket函数创建一个套接字并返回一个整型描述符:
int socket(int Protofamily,int Type,int Protocol);
1.Protofamily应该设置成“AF_INET”。
2.参数type告诉内核是SOCK STREAM类型还是SOCK DGRAM类型。
3.把protocol设置为"0"。
4.socket()返回socket描述符,或者在错误的时候返回-l。
#include <sys/types.h> #include <sys/socket.h> // 设置套接字选项 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)
sockfd
:标识一个套接口的描述字。 level
:选项定义的层次;支持SOL_SOCKET
、IPPROTO_TCP
、IPPROTO_IP
和IPPROTO_IPV6
等不同的层次。 optname
:需要设置的选项。 optval
:指针,指向存放选项值的缓冲区。 optlen
:optval缓冲区的长度。
有两种类型的套接口选项:
布尔型选项,允许或禁止一种特性。
允许一个布尔型选项,则将optval指向非零整数。
禁止一个选项,则optval指向一个等于零的整数。
对于布尔型选项,optlen应该等于sizeof(int)。
整数或结构选项。
optval指向包含所需选项的整数或结构的指针。
optlen则为整数或结构的长度。
setsockopt()支持下列选项。其中"类型”表明optval所指数据类型。
选项 | 类型 | 含义 |
---|---|---|
SO_BROADCAST | BOOL | 允许套接口传送广播信息。 |
SO_DEBUG | BOOL | 记录调试信息。 |
SO_DONTLINGER | BOOL | 不要因为数据未发送就阻塞关闭操作。 |
SO_DONTROUTE | BOOL | 禁止选径;直接传送。 |
SO_KEEPALIVE | BOOL | 发送 "保持活动" 包。 |
SO_LINGER | struct linger FAR* | 如关闭时有未发送数据,则逗留。 |
SO_OOBINLINE | BOOL | 在常规数据流中接收带外数据。 |
SO_RCVBUF | int | 为接收确定缓冲区大小。 |
SO_REUSEADDR | BOOL | 允许套接口和一个已在使用中的地址捆绑。 |
SO_SNDBUF | int | 指定发送缓冲区大小。 |
TCP_NODELAY | BOOL | 禁止发送合并的 Nagle 算法。 |
若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
例如:
// 设置接收缓冲区大小为32K int nRecvBufLen = 32 * 1024; // 设置为32K setsockopt(s, SOL_SOCKET, SO_RCVBUF, (const char*)&nRecvBufLen, sizeof(int));// 设置发送缓冲区大小为32K int nSendBufLen = 32 * 1024; // 设置为32K setsockopt(s, SOL_SOCKET, SO_SNDBUF, (const char*)&nSendBufLen, sizeof(int));
议栈在调用 setsockopt
时会与 rmem_max
进行比较。rmem_max
是一个内核调优参数,可以通过 /proc/sys/net/core/rmem_max
进行调整。rmem_max
表示内核接受数据缓冲区的最大大小。
mem_max
是内核接受数据缓冲的大小,它的默认值与内核版本有关。例如,在Linux 2.6.2版本中,mem_max
的默认值是 65535。需要注意的是,mem_max
表示的是缓冲区的大小,而不是一次要接收的数据的大小。
应用程序可以通过调用 recv
函数的参数来指定一次接收的数据大小,但指定的参数最大不会超过 mem_max
。内核会根据 mem_max
来缓存接收到的数据,并在应用程序调用 recv
函数时进行处理。
2、绑定套接字到指定的地址BIND()---打开一个通道(第2步)
int bind(int sockfd,struct sockaddr*My addr,int Addrlen);
sockfd
是调用 socket
返回的文件描述符。
myaddr
是指向数据结构 struct sockaddr
的指针,用于保存地址信息,包括端口和IP地址。
addrlen
设置为 sizeof(struct sockaddr)
。
#include <string.h> #include <sys/types.h> #include <sys/socket.h> #define MYPORT 3490int main() {int sockfd;struct sockaddr_in my_addr;// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);// 进行错误检查,确保套接字创建成功if (sockfd == -1) {perror("Socket creation failed");return 1; // 或者采取适当的错误处理措施}// 设置地址结构体my_addr.sin_family = AF_INET; // 使用IPv4地址my_addr.sin_port = htons(MYPORT); // 设置端口号,注意字节序转换my_addr.sin_addr.s_addr = inet_addr("132.241.5.10"); // 设置IP地址bzero(&(my_addr.sin_zero), 8); // 清零结构体的其余部分// 进行错误检查,确保绑定成功if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) {perror("Bind failed");return 1; // 或者采取适当的错误处理措施}// 其他操作...return 0; }
my_addr.sin_port
是网络字节顺序,my_addr.sin_addr.s_addr
也是的。
在处理自己的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。
3、启动监听Listen()---等待通信请求
int listen(int sockfd,int backlog);
sockfd
是调用 socket
返回的套接口文件描述符。
backlog
是在进入队列中允许的连接数目(queue of pending connections)。
描述:
listen
函数用于将套接字设置为监听状态,以等待传入的连接请求。在调用 listen
后,套接字将能够接受客户端的连接请求,并将它们放入两个不同的队列中:
未完成连接队列(SYN_RCVD):维护等待完成三次握手的连接,这些连接已经收到了客户端的SYN报文,但尚未建立完全的连接。
已连接队列(ESTABLISHED):包含已经完成三次握手的连接,这些连接已经准备好进行通信。
backlog
参数指定了允许在未完成连接队列中排队的连接数目。如果队列已满,新的连接请求将被拒绝。
返回值:
如果成功,返回0。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
示例:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h>int main() {int sockfd;// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 设置套接字为监听状态,backlog为5if (listen(sockfd, 5) == -1) {perror("Listen failed");exit(EXIT_FAILURE);}printf("n");// 其他操作...return 0; }
上述示例演示了如何创建套接字并将其设置为监听状态,以等待传入的连接请求。在此示例中,backlog
参数被设置为5,允许最多排队5个未完成连接。
在TCP通信中,连接队列的实际长度由系统常量 SOMAXCONN
决定,默认值为128,并且可以通过参数 backlog
共同决定。
当 backlog
的值小于 SOMAXCONN
时,已完成连接队列的数量最多为 backlog
的值,未完成连接队列的数量大约在10左右。
当 backlog
的值大于等于 SOMAXCONN
时,已完成连接队列的数量最多为 SOMAXCONN
,未完成连接队列的数量仍然大约在10左右。
在客户端的第一个SYN到达时,TCP会在未完成连接队列中增加一个新的记录,然后回复给客户端三次握手的第二个报文(服务端的SYN和针对客户端的ACK)。这个记录将一直存在于未完成连接队列中,直到三次握手中的最后一个报文到达,或者直到发生超时。在Berkeley套接字实现中,超时被定义为75秒。
如果在客户端的SYN到达时,未完成连接队列已经满了,TCP将忽略后续到达的SYN报文,但不会发送RST信息给客户端,这允许客户端重传SYN报文以尝试重新建立连接。
举例:LISTEN(Sockfe,3); 监听套接字使用缓冲区接纳多个客户端的连接请求
4、接收连接请求ACCEPT()---接收服务请求
int accept(int sockfd, struct sockaddr *addr, int *addrlen);
参数:
sockfd
:用于监听客户端连接请求的套接字描述符。
addr
:sockaddr
结构变量的指针,是一个输出型参数。当函数执行完成后,这个变量中包含所接收的客户端的地址信息。
addrlen
:输出型参数,调用函数时需要初始化为 addr
结构的长度,不能为0或NULL。执行完毕后,返回所接收客户端网络地址的长度。
描述:
accept
函数用于接受客户端的连接请求,它会等待客户端连接并返回一个新的套接字描述符(称为响应套接字)。这个新的套接字已经与客户端建立了连接,并可以用于以后与客户端进行数据交换。
返回值:
如果执行正确,accept
函数返回新的套接字描述符(响应套接字)。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
accept
函数通常在服务器端用于接受连接请求。一旦连接被接受,服务器可以使用返回的新套接字来与客户端进行通信。
addr
参数用于存储客户端的地址信息,包括IP地址和端口号。
addrlen
参数在调用函数之前应该初始化为 addr
结构的长度。函数执行完成后,它会包含客户端地址的实际长度。
示例:
// 假设已经创建了监听套接字 listenfd int clientfd; // 定义响应套接字描述符变量 int addrlen = sizeof(struct sockaddr); // 获取套接字地址结构长度 struct sockaddr_in cltsockaddr; // 定义用于返回客户端地址的结构clientfd = accept(listenfd, (struct sockaddr*)(&cltsockaddr), &addrlen); // 接收客户连接请求
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h>int main() {int sockfd, new_sock;struct sockaddr_in server_addr, client_addr;socklen_t addr_len = sizeof(client_addr);// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 绑定套接字到端口server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("Bind failed");exit(EXIT_FAILURE);}// 监听连接请求if (listen(sockfd, 5) == -1) {perror("Listen failed");exit(EXIT_FAILURE);}printf("n");// 接受连接请求new_sock = accept(sockfd, (struct sockaddr *)&client_addr, &addr_len);if (new_sock == -1) {perror("Accept failed");exit(EXIT_FAILURE);}printf("Accepted connection from %s:%dn", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));// 其他操作...return 0; }
5、请求建立连接CONNECT()---建立连接请求
int connect(int sockfd, struct sockaddr *server_addr, int addrlen);
sockfd
:客户端生成并安装的套接字描述符(请求套接字)。
server_addr
:存放服务器端的网络地址。
addrlen
:sockaddr
结构的长度。
描述:
connect
函数用于客户端建立与服务器端的连接。它将客户端的套接字(请求套接字)与服务器的套接字进行连接。在调用 connect
之前,客户端无需进行 bind
调用,因为内核将选择一个合适的端口号并将其与当前套接字绑定。
返回值:
如果连接成功,返回0。
如果连接失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
connect
函数通常在客户端用于建立与服务器的连接。一旦连接成功,客户端可以使用返回的套接字来与服务器进行数据通信。
server_addr
参数用于指定服务器端的网络地址信息,包括IP地址和端口号。
addrlen
参数应该设置为 server_addr
结构的长度。
示例:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <netinet/in.h>int main() {int sockfd;struct sockaddr_in server_addr;// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 设置服务器地址信息server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8080);server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");// 连接到服务器if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("Connect failed");exit(EXIT_FAILURE);}printf("Connected to servern");// 其他操作...return 0; }
6、读/写套接字read
函数和write
函数
read
函数
int read(int sockfd, void *buffer, int len);
sockfd
:要读取的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。
buffer
:指向内存中用于存放数据的读取缓冲区。
len
:读取缓冲区的长度,或者希望读取的字符长度。
描述:
read
函数用于从套接字中读取数据。它从指定的套接字 sockfd
中读取数据,并将其存储到指定的缓冲区 buffer
中。len
参数表示要读取的数据的长度。
返回值:
如果成功,返回已读取的字节数。
如果达到文件末尾,返回0。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
read
函数必须用于已连接的套接字,即在成功建立连接之后才能使用。
通常在客户端用于从服务器端读取响应数据。
write
函数
int write(int sockfd, const void *buffer, int len);
sockfd
:要写入的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。
buffer
:指向内存中存储要写入的数据的缓冲区。
len
:要写入的数据的长度。
描述:
write
函数用于向套接字中写入数据。它将指定的数据从缓冲区 buffer
写入到套接字 sockfd
中,写入的数据长度由参数 len
指定。
返回值:
如果成功,返回已写入的字节数。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
write
函数必须用于已连接的套接字,即在成功建立连接之后才能使用。
通常在客户端用于向服务器端发送请求数据。
示例:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h>int main() {int sockfd;char buffer[1024];// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 其他操作...// 读取数据int bytesRead = read(sockfd, buffer, sizeof(buffer));if (bytesRead == -1) {perror("Read failed");exit(EXIT_FAILURE);}// 写入数据int bytesWritten = write(sockfd, buffer, bytesRead);if (bytesWritten == -1) {perror("Write failed");exit(EXIT_FAILURE);}// 其他操作...return 0; }
7. 向套接字发送 SEND() 和从套接字接收 RECV()
send
函数
int send(int sockfd, const void *buf, int len, int flags);
sockfd
:要写入的套接字描述符。在客户端是请求套接字,在服务器端是响应套接字。
buf
:指向内存中存储要发送的数据的缓冲区。
len
:要发送的数据的长度。
flags
:执行本调用的方式,通常置为0。
描述:
send
函数用于向套接字中发送数据。它将指定的数据从缓冲区 buf
发送到套接字 sockfd
中,发送的数据长度由参数 len
指定。
返回值:
如果成功,返回已发送的字节数。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
send()
函数在调用后会返回实际发送数据的长度。
send
函数必须用于已连接的套接字,即在成功建立连接之后才能使用。
通常在客户端用于向服务器端发送请求数据。
send()
函数默认工作在阻塞模式下。
在非阻塞情况下,send()
所发送的数据可能少于你给它的参数所指定的长度!这是因为如果你给 send()
的参数中包含的数据的长度远远大于 send()
所能一次发送的数据,那么 send()
函数只发送它所能发送的最大数据长度,然后对剩下的数据再次调用它来进行第二次发送。
因此,记住如果 send()
函数的返回值小于 len
的话,你需要再次发送剩下的数据。幸运的是,大多数情况下,send()
都会一次发送成功。
recv
函数
int recv(int sockfd, void *buf, int len, int flags);
sockfd
:要读取的套接字描述符。在客户端是响应套接字,在服务器端是请求套接字。
buf
:指向内存中用于存放接收数据的缓冲区。
len
:接收缓冲区的长度,或者希望接收的字符长度。
flags
:执行本调用的方式,通常置为0。
描述:
recv
函数用于从套接字中接收数据。它从指定的套接字 sockfd
中接收数据,并将其存储到指定的缓冲区 buf
中,接收的数据长度由参数 len
指定。
返回值:
如果成功,返回已接收的字节数。
如果连接已关闭,返回0。
如果失败,返回-1,并设置全局变量 errno
以指示错误的类型。
注意:
recv
函数必须用于已连接的套接字,即在成功建立连接之后才能使用。
通常在客户端用于从服务器端接收响应数据。
recv()
返回它所真正收到的数据的长度(也就是存储到 buf
中的数据的长度)。
如果 recv()
返回 -1,则代表发生了错误。
示例:
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <unistd.h>int main() {int sockfd;char buffer[1024];// 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1) {perror("Socket creation failed");exit(EXIT_FAILURE);}// 其他操作...// 发送数据int bytesSent = send(sockfd, buffer, sizeof(buffer), 0);if (bytesSent == -1) {perror("Send failed");exit(EXIT_FAILURE);}// 接收数据int bytesRead = recv(sockfd, buffer, sizeof(buffer), 0);if (bytesRead == -1) {perror("Receive failed");exit(EXIT_FAILURE);}// 其他操作...return 0; }
send
函数执行流程send
函数首先检查协议是否正在发送套接字s
的发送缓冲区中的数据。
如果协议正在发送数据,send
函数等待协议将数据发送完毕。
如果协议尚未开始发送套接字s
的发送缓冲区中的数据,或者发送缓冲区中没有数据,那么 send
函数比较套接字s
的发送缓冲区的剩余空间和参数 len
。
如果 len
大于剩余空间大小,send
函数一直等待协议将套接字s
的发送缓冲中的数据发送完毕。
如果 len
小于剩余空间大小,send
函数仅将 buf
中的数据复制到剩余空间中(注意,send
不会传输套接字s
的发送缓冲区中的数据到连接的另一端,而是由协议传输,send
仅将 buf
中的数据复制到套接字s
的发送缓冲区的剩余空间中)。
如果 send
函数成功复制数据,就返回实际复制的字节数。
如果 send
在复制数据时出现错误,那么 send
函数返回 SOCKET_ERROR
。
如果 send
在等待协议传输数据时发生网络断开,那么 send
函数也返回 SOCKET_ERROR
。
需要注意的是,send
函数在成功复制数据到套接字s
的发送缓冲区的剩余空间后立即返回,但这些数据不一定立即传输到连接的另一端。如果协议在后续的传输过程中出现网络错误,下一个Socket函数将返回 SOCKET_ERROR
。(每一个除 send
外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传输完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR
。)
recv
函数执行流程当应用程序调用 recv
函数时,recv
先等待套接字s
的发送缓冲中的数据被协议传输完毕。如果协议在传输套接字s
的发送缓冲中的数据时出现网络错误,那么 recv
函数返回 SOCKET_ERROR
。
如果套接字s
的发送缓冲中没有数据,或者数据被协议成功传输完毕后,recv
先检查套接字s
的接收缓冲区。
如果套接字s
的接收缓冲区中没有数据,或者协议正在接收数据,recv
将一直等待,直到协议把数据接收完毕。
当协议接收到数据后,recv
函数将套接字s
的接收缓冲中的数据复制到 buf
中(注意,协议接收到的数据可能大于 buf
的长度,因此在这种情况下需要多次调用 recv
函数才能完全复制套接字s
的接收缓冲中的数据)。recv
函数返回实际复制的字节数。
如果 recv
在复制数据时出现错误,那么它返回 SOCKET_ERROR
。
如果 recv
函数在等待协议接收数据时网络断开,它将返回0。
8. 关闭套接字CLOSE()
CLOSE()
函数
int CLOSE(int sockfd);
CLOSE()
函数的调用将阻止在套接口上进一步的数据读写。任何在另一端尝试读写套接口的操作都将返回错误信息。
shutdown()
函数
int shutdown(int sockfd, int how);
shutdown()
函数允许你有更多控制关闭套接口的方式。它可以关闭特定方向的通信,或关闭双向通信(与close()
函数类似)。
参数说明:
sockfd
:要关闭的套接口文件描述符。
how
的值可以是以下之一:
0 - 禁止进一步的接收操作。
1 - 禁止进一步的发送操作。
2 - 禁止进一步的发送和接收操作。
使用shutdown()
函数,你可以更具体地控制套接口的关闭方式,允许你选择禁止数据接收、数据发送或同时禁止两者。
1.实例的功能
服务器对来访的客户计数,并向客户报告这个计数值。
客户建立与服务器的一个连接并等待它的输出。
每当连接请求到达时,服务器生成一个可打印的ASCII串信息,将它在连接上发回,然后关闭连接。
客户显示收到的信息,然后退出。例如,对于服务器接收的第10次客户连接请求,该客户将收到并打印如下信息:
This server has been contacted 10 times.
2.实例程序的命令行参数
实例是UNIX环境下的C程序,客户和服务器程序在编译后,均以命令行的方式执行。
服务器程序执行时可以带一个命令行参数,是用来接受请求的监听套接字的协议端口号。这个参数是可选的。如果不指定端口号,代码将使用程序内定的缺省端口号5188。
客户程序执行时可以带两个命令行参数: 一个是服务器所在计算 机的主机名 , 另一个是服务器监听的协议端口号。
这两个参数都是可选的。
如果没有指定协议端口号 ,客户使用程序内定的缺省值5188。
如果一个参数也没有 ,客户使用缺省端口和主机名localhost , localhost是映射到客户所运行的计算机的一个别名。
允许客户与本地机上的服务器通信 , 对调试是很有用的。
3.客户程序代码
/*---------------------------------------------------- * 程序: client.c * 目的: 创建一个套接字,通过网络连接一个服务器,并打印来自服务器的信息 * 语法: client [ host [ port ] ] * host - 运行服务器的计算机的名字 * port - 服务器监听套接字所用协议端口号 * 注意:两个参数都是可选的。如果未指定主机名,客户使用localhost;如果未指定端口号, * 客户将使用PROTOPORT中给定的缺省协议端口号 *---------------------------------------------------- */#include <sys/types.h> #include <sys/socket.h> /* UNIX下,套接字的相关包含文件。*/ #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <stdio.h> #include <string.h>#define PROTOPORT 5188 /*缺省协议端口号*/ extern int errno;char localhost = "localhost"; /*缺省主机名*/main(argc,argv) int argc; char *argv[]; {struct hostent *ptrh; /* 指向主机列表中一个条目的指针 */struct sockaddr_in servaddr; /* 存放服务器端网络地址的结构 */int sockfd;/* 客户端的套接字描述符 */int port; /* 服务器端套接字协议端口号*/char* host;/* 服务器主机名指针 */int n;/* 读取的字符数 */char buf[1000];/* 缓冲区,接收服务器发来的数据 */memset((char*)& servaddr,0,sizeof(servaddr)); /* 清空sockaddr结构 */ servaddr.sin_family = AF_INET; /* 设置为因特网协议族 *//* 检查命令行参数,如果有,就抽取端口号。否则使用内定的缺省值*/if (argc>2){port = atoi(argv[2]); /* 如果指定了协议端口,就转换成整数 */}else {port = PROTOPORT; /* 否则,使用缺省端口号 */}if (port>0) /* 如果端口号是合法的数值,就将它装入网络地址结构 */servaddr.sin_port = htons((u_short)port);else{ /* 否则,打印错误信息并退出*/fprintf(stderr,”bad port number %sn” ,argv[2]);exit(1);}/* 检查主机参数并指定主机名 */if(argc>1){host = argv[1]; /* 如果指定了主机名参数,就使用它 */}else{host = localhost; /* 否则,使用缺省值 */}/* 将主机名转换成相应的IP地址并复制到servaddr 结构中 */ptrh = gethostbyname( host ); /* 从服务器主机名得到相应的IP地址 */if ( (char *)ptrh == null ) { /* 检查主机名的有效性,无效则退出 */fprintf( std err, ” invalid host: %sn” , host );exit(1);}memcpy(&servaddr.sin_addr, ptrh->h_addr, ptrh->h_length );/* 创建一个套接字*/sockfd = SOCKET(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {fprintf(stderr, ”socket creation failedn” );exit(1);}/* 请求连接到服务器 */if (connect( sockfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) {fprintf( stderr,”connect failedn” );/* 连接请求被拒绝,报错并退出 */exit(1);}/* 从套接字反复读数据,并输出到用户屏幕上 */n = recv(sockfd , buf, sizeof( buf ), 0 );while ( n > 0) {write(1 ,buf, n);/* unix I/O文件句柄: 0 std in;1 std out;2 std err */n = recv( sockfd , buf, sizeof( buf ), 0 );}/* 关闭套接字*/closesocket( sockfd );/* 终止客户程序*/exit(0); }
4. 服务器实例代码
/*---------------------------------------------------- * 程序:server.c * 目的: 分配一个套接字,然后反复执行如下几步: * (1) 等待客户的下一个连接 * (2) 发送一个短消息给客户 * (3) 关闭与客户的连接 * (4) 转向(1)步 * 命令行语法: server [ port ] * port - 服务器端监听套接字使用的协议端口号 * 注意: 端口号可选。如果未指定端口号,服务器使用PROTOPORT中指定的缺省 * 端口号 *--------------------------------------------------------------- */#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <string.h> #define PROTOPORT 5188 /* 监听套接字的缺省协议端口号 */ #define QLEN 6 /* 监听套接字的请求队列大小 */int visits = 0; /* 对于客户连接的计数*/main(argc,argc) int argc; char* argv[]; {struct hostent *ptrh;/*指向主机列表中一个条目的指针*/struct sockaddr_in servaddr;/*存放服务器网络地址的结构*/struct sockaddr_in clientaddr;/*存放客户网络地址的结构*/int listenfd;/*监听套接字描述符*/int clientfd;/*响应套接字描述符*/int port;/*协议端口号*/int alen;/*地址长度*/char buf[1000];/*供服务器发送字符串所用的缓冲区*/memset( (char*)& servaddr, 0, sizeof(servaddr) ); /* 清空sockaddr结构 */servaddr.sin_family = AF_INET; /* 设置为因特网协议族 */servaddr.sin_addr.s_addr = INADDR_ANY; /* 设置本地IP地址 *//* 检查命令行参数,如果指定了,就是用该端口号,否则使用缺省端口号 */if (argc > 1){port = atoi(argv[1]); /* 如果指定了端口号,就将它转换成整数 */} else {port = PROTOPORT; /* 否则,使用缺省端口号 */}if (port > 0) /* 测试端口号是否合法 */servaddr.sin_port=htons( (u_short)port );else{ /* 打印错误信息并退出 */fprintf( stderr, "bad port number %sn", argv[1] );exit(1);}/* 创建一个用于监听的流式套接字 */listenfd = SOCKET(AF_INET,SOCK_STREAM,0);if (listenfd <0) {fprintf( stderr, "socket creation failedn" );exit(1);}/* 将本地地址绑定到监听套接字*/if ( bind( listenfd, (struct sockaddr *)& servaddr, sizeof(servaddr)) < 0) {fprintf(stderr, "bind failedn" );exit(1);}/* 开始监听,并指定监听套接字请求队列的长度 */if (listen(listenfd, QLEN) < 0) {fprintf(stderr, "listen filedn" );exit(1);}/* 服务器主循环—接受和处理来自客户端的连接请求 */while(1) {alen = sizeof(clientaddr); /* 接受客户端连接请求,生成响应套接字 */if((clientfd = accept( listenfd, (struct sockaddr *)& clientaddr, &alen)) < 0 ) {fprintf( stderr, "accept failedn");exit(1);}visits++; /* 累加访问的客户数 */sprintf( buf, "this server has been contacted %d time n", visits ); send(clientfd, buf, strlen(buf), 0 ); /* 向客户端发送信息 */closesocket( clientfd ); /* 关闭响应套接字 */} }
说明:
1 、客户机代码为何反复的调用recv来获取数据?
SOCK_STREAM套接字不保证每个recv调用返回的数据恰好是服务器在send调用中所发送的数据量。
2 、阻塞式套接字函数与进程阻塞
当应用进程调用阻塞式套接字函数时,如不能直接返回,则进程暂停执行直至函数结束而返回,进程才能继续运行下去。
当套接字函数不能及时完成而返回,进程就因此而处于等待状态,等待时间取决于套接字过程。当进程处于这种等待状态时,就说明该进程被阻塞了。
计算机中运行着 A 、B 与 C 三个进程,其 中进程 A 执行网络程序,一开始,这 3 个 进程都被操作系统的工作队列所引用,处 于运行状态,会分时执行。
当进程 A 执行到创建 Socket 的语句时, 操作系统会创建一个由文件系统管理的 Socket 对象
当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中。 由于工作队列只 剩下了进程 B 和 C ,依据进程调度,CPU 会轮流执行 这两个进程的程序,不会执行进程 A 的程序。所以进 程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。
计算机收到了对端传送的数据,数据经由网卡传送到内存, 然后网卡通过中断信号通知 CPU 有数据到达,CPU 执行 中断程序,此处的中断程序主要有两项功能,先将网络数据写入到对应 Socket 的接收缓冲区里面,再唤醒进程 A,重新将进程 A 放入工作队列中。
当 Socket 接收到数据后,操作系统将该 Socket 等待队列 上的进程重新放回到工作队列,该进程变成运行状态,继 续执行代码。同时由于 Socket 的接收缓冲区已经有了数据, Recv 可以返回接收到的数据。
在C语言中,在调用API函数发生异常时,一般会将errno
变量(需要包含errno.h
头文件)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。但是errno
是一个数字,代表的具体含义还要到errno.h
中去阅读宏定义,而每次查阅是一件很繁琐的事情。有下面几种方法可以方便地得到错误信息:
perror
函数:
perror()
用来将上一个函数发生错误的原因输出到标准错误(stderr
),参数s
所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno
的值来决定要输出的字符串。
strerror
函数:
strerror
函数用于将错误代码转换为字符串错误信息,可以将该字符串和其他的信息组合输出到用户界面。例如
fprintf(stderr, "error in CreateProcess %s", strerror(errno))
。
printf("%m", errno)
:
这种方式也可以用于获取错误信息。
使用h_errno
:
使用gethostbyname()
和gethostbyaddr()
函数时,不能使用perror()
来输出错误信息,因为错误代码存储在h_errno
中而不是errno
中。h_errno
也是一个外部整型变量。所以,需要调用herror()
函数。需要包含头文件netdb.h
。
void herror(const char *s);
herror()
用来将上一个网络函数发生错误的原因输出到标准错误(stderr
)。参数s
所指的字符串会先打印出,后面再加上错误的原因字符串。此错误原因系依照全局变量h_errno
的值来决定要输出的字符串。
在Win32平台上,可以使用GetLastError()函数来获取上一个函数操作时所产生的错误代码。通过错误代码,可以在winerror.h
头文件中查找到每一中错误代码所代表的含义。你也可以使用VC++自带的Error Lookup工具来查找其所表示的含义,结果是一样的。
函数签名:
DWORD GetLastError(void);
这是一个没有参数的函数,通过调用,就返回一个32位的数值。
描述:
GetLastError()
函数是一个没有参数的函数,调用它将返回一个32位的数值,即上一个函数的错误代码。这个错误代码可以用来诊断和处理函数操作中的错误情况。
1.什么是阻塞阻塞
阻塞是指一个进程执行了一个函数或者系统调用,该函数由于某种原因不能立即完成, 因而不能返回调用它的进程,导致进程受控于这 个函数而处于等待的状态,进程的这种状态称为阻塞。
2. 能引起阻塞的套接字调用
在Berkeley套接字网络编程接口的模型中,套接字的默认行为是阻塞的,具 体地说,在一定情况下,有多个操作套接字的系统调用会引起进程阻塞。
(1)ACCEPT()(2)READ()、RECV()和READFORM()(3)WRITE()、SEND()和SENDTO()(4)CONNECT()(5)SELECT()(6)CLOSESOCKET()
3. 阻塞工作模式带来的问题
采用阻塞工作模式的单进程服务器是不能很好地同时为多个客户服务的
4. 一种解决方案
利用UNIX操作系统的fork()
系统调用编写多进程并发执行的服务器程序
你可以使用UNIX操作系统的fork()
系统调用来编写多进程并发执行的服务器程序。这允许你创建多个子进程,为每个客户端提供专门的服务。通过并发执行的进程,你可以实现对多个客户端的并发服务。下面是一些基本的编程框架:
#include <sys/types.h> // 提供类型pid_t的定义 #include <unistd.h> // 提供函数的定义 pid_t fork(void);
fork
系统调用的作用是复制一个进程。当一个进程调用它后,会创建一个几乎与原始进程相同的新进程。这意味着你会有两个几乎一模一样的进程,一个是原始进程,另一个是新进程。这也是为什么fork
的名字与叉子的形状有点相似,因为它分叉了进程的工作流程。
/* fork_test.c */ #include <sys/types.h> #include <unistd.h>int main() {pid_t pid; /* 此时仅有一个进程 */pid = fork(); /* 此时已经有两个进程在同时运行 */if (pid < 0)printf("Error in fork!n");else if (pid == 0)printf("I am the child process, my process ID is %dn", getpid());elseprintf("I am the parent process, my process ID is %dn", getpid());return 0; }
这个程序会创建两个进程,一个父进程和一个子进程,它们将同时运行。父进程会输出它自己的进程ID,而子进程会输出它自己的进程ID。你可以编译并运行这个程序来看到输出结果。
编译并运行程序的示例命令:
$ gcc fork_test.c -o fork_test $ ./fork_test
输出结果可能会类似于:
I am the parent process, my process ID is 1991 I am the child process, my process ID is 1992
这表明父进程和子进程都在同时运行,并且它们有不同的进程ID。
看这个程序的时候,头脑中必须首先了解一个概念:在语句pid=fork()之前,只有一个进程在执行这段代 码,但在这条语句之后,就变成两个进程在执行了,这两个进程的代码部分完全相同,将要执行的下一 条语句都是if(pid==0) … …。
两个进程中,原先就存在的那个被称作“父进程 ”,新出现的那个被称作“子进程 ”。父子进程的区别 除了进程标志符(process ID)不同外,变量pid的值也不相同,pid存放的是fork的返回值。fork调用的一 个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
在父进程中,fork返回新创建子进程的进程ID; 在子进程中,fork返回0; 如果出现错误,fork返回一个负值;
fork出错可能有两种原因:
(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为 EAGAIN 。
(2)系统内存不足,这时errno的值被设置为ENOMEM。
当第一次调用 socket()
建立套接口描述符的时候,内核就将相关的函数设置为阻塞。如果不想套接口阻塞,就要调用函数 fcntl()
:
#include <unistd.h> #include <fcntl.h>sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK);
通过设置套接口为非阻塞,能够有效地“询问”套接口以获得信息。如果尝试着从一个非阻塞的套接口读信息并且没有任何数据,这时不会变成阻塞--将返回 -1 并将 errno
设置为 EWOULDBLOCK
。
但是一般说来,轮询不是个好主意。如果让程序在忙等状态查询套接口的数据,将浪费大量的 CPU 时间。更好的解决之道是用 select()
去查询是否有数据要读进来。
select()
函数可以同时监视多个套接字。它会告诉你哪个套接字已准备好读取,哪个准备好写入,哪个套接字发生了异常。
函数签名
#include <sys/time.h> #include <sys/types.h> #include <unistd.h>int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
返回值
负值:select()
函数发生错误。
正值:某些文件描述符(套接字)已准备好读取、写入或发生错误。
0:等待超时,没有可读、可写或出错的文件描述符。
参数
numfds
:文件描述符集中所有文件描述符的范围,即所有文件描述符的最大值加1。
readfds
:用于监视可读文件描述符的集合。
writefds
:用于监视可写文件描述符的集合。
exceptfds
:用于监视异常文件描述符的集合。
注意
当函数 select()
返回时,readfds
的值会被修改,以反映哪个文件描述符可以读取。可以使用宏 FD_ISSET()
来测试。
FD_ZERO(fd_set *set)
:清除一个文件描述符集合,将集合中的所有文件描述符清零。
FD_SET(int fd, fd_set *set)
:将文件描述符 fd
添加到集合中,表示你要监视这个文件描述符的状态。
FD_CLR(int fd, fd_set *set)
:从集合中移除文件描述符 fd
,表示你不再监视这个文件描述符的状态。
FD_ISSET(int fd, fd_set *set)
:测试文件描述符 fd
是否在集合中,如果在集合中返回非零值(true),否则返回零值(false)。
结构体 struct timeval
在多路复用操作中,有时你不想永远等待,而是希望在一定时间内执行某个操作。结构体 struct timeval
允许你设置一个等待的时间,如果在这个时间内没有发生某种事件,就可以继续处理其他任务。
struct timeval {int tv_sec; /* seconds */int tv_usec; /* microseconds */ };
你可以将 tv_sec
设置为你要等待的秒数,将 tv_usec
设置为你要等待的微秒数。当函数返回时,timeout
可能是剩余的时间。
• 标准的 Unix 系统的时间片是100毫秒,所以无论如何设置struct timeval ,都要等待 那么长的时间。
• 如果设置 struct timeval 中的数据为 0 ,select() 将立即超时,这样就可以有效地轮询集合中的所有的文件描述符。如果将参数 timeout 赋值为 NULL ,那么将永远不会 发生超时,即一直等到第一个文件描述符就绪。
• 如果你有一个正在侦听 (listen()) 的套 接字,你可以通过将该套接字的文件描述符 加入到 readfds 集合中来看是 否有新的连接。
使用 fd_set 集合
在多路复用中,你可以使用 fd_set
集合来监视多个文件描述符的状态,然后选择性地等待它们的状态变化。以下是一些操作 fd_set
集合的宏:
FD_ZERO(fd_set *set)
:清除一个文件描述符集合,将集合中的所有文件描述符清零。
FD_SET(int fd, fd_set *set)
:将文件描述符 fd
添加到集合中,表示你要监视这个文件描述符的状态。
FD_CLR(int fd, fd_set *set)
:从集合中移除文件描述符 fd
,表示你不再监视这个文件描述符的状态。
FD_ISSET(int fd, fd_set *set)
:测试文件描述符 fd
是否在集合中,如果在集合中返回非零值(true),否则返回零值(false)。
这些宏通常用于设置要监视的文件描述符,然后通过 select()
函数等来等待这些文件描述符的状态变化。例如,在以下示例中,我们使用 select()
来等待多个套接字的状态变化:
fd_set fdread; timeval tv = {1, 0}; while (1) {// 初始化 fd_setFD_ZERO(&fdread);for (int i = 0; i < nSock; i++)FD_SET(socks[i], &fdread);// 等待事件触发,或超时返回int ret = select(numfds, &fdread, NULL, NULL, &tv);for (int i = 0; ret > 0 && i < nSock; i++)// 检测哪个 sock 有事件触发if (FD_ISSET(socks[i], &fdread)) {read_buf(socks[i]);ret--;} }
在Linux中,fd_set
结构体通常通过位域来表示文件描述符的集合。每个位域表示一个文件描述符,而一个 unsigned long
整数可以包含多个位域。这使得 fd_set
可以表示大量的文件描述符。
使用数据报套接字开发网络应用程序,既可以采用客户/ 服务器模式,也可以采用对等模式。
1. 发送数据报SENDTO()
int SENDTO( int sockfd, const void* msg, int len, unsigned int flags, struct sock a ddr* to, int tolen);
2. 接收数据报 RECVFROM()
int RECVFROM( int sockfd, void* bu f, int len, unsigned int flags, struct sock a ddr* from, int* fromlen )
课本45
利用原始套接字可以绕过传输层直接访问IP协议、ICMP协议和IGMP协议.
目前,只有Winsock2提供了对原始套接字的支持,并将原始套接字称为 SOCK_RAW类型,操作系统应使用Windows2000以上版本。
1、原始套接字的创建
格式一:
int sock Raw =socket(AF_INET,SOCK_RAW,protocol)
格式二:
SOCKET sock Raw =WSASocket(AF_INET,SOCK_RAW,protocol,NULL,0,0)
其中 ,参数protocol用来指定协议类型 , 可取如下值:
IPPROTO_ICMP | ICMP协议 | IPPROTO_UDP | UDP协议 |
---|---|---|---|
IPPROTO_IGMP | IGMP协议 | IPPROTO_IP | IP协议 |
IPPROTO_TCP | TCP协议 | IPPROTO_RAW | 原始IP |
2.原始套接字的使用
设置套接字选项
在使用原始套接字时,你可能需要根据需要设置套接字选项,以满足你的特定需求。你可以使用 setsockopt
函数来设置套接字选项。
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
sock
:要设置或获取选项的套接字。
level
:选项所在的协议层。.可以取三种值: SOL_SOCKET
:通用套接字选项 IPPROTO_IP
:IP选项 IPPROTO_TCP
:TCP选项.
optname
:指定控制的方式(选项的名称)
optval
:设置套接字选项 , 根据选项名称的数据类型进行转换
optlen
:选项值的长度。
使用 setsockopt
函数,你可以根据需要配置套接字的选项,以满足你的通信需求。这可以包括设置套接字的各种属性,例如缓冲区大小、超时设置、广播选项等。
选项名称 | 说明 | 数据类型 |
---|---|---|
IPPROTO_IP | ||
IP_HDRINCL | 在数据包中包含 IP 首部 | int |
IP_OPTIONS | IP 首部选项 | int |
IP_TOS | 服务类型 | int |
IP_TTL | 生存时间 | int |
IPPRO_TCP | ||
TCP_MAXSEG | TCP最大数据段的大小 | int |
TCP_NODELAY | 不使用Nagle算法 | int |
(2) 调用connect和bind函数来绑定对方和本地地址
(3) 发送数据包
如果没有使用connect函数绑定对方地址 ,则应使用sendto或send msg函数发送数据包。
如果调用了connect函数 ,则可以直接使用send、write来发送数据包。
如果设置了IP_HDRINCL选项 ,则包内要填充的内容为数据部分和IP首部 , 内 核只负责填充数据包的标志域和首部校验和。
注意 , IP数据包首部各个域的内容是网络字节序
以下是关于接收数据包的信息的Markdown格式输出:
(4)接收数据包
接收数据包的内核遵循以下规则:
UDP 和 TCP 数据包通常不会传递给原始套接字。要查看这两种数据包,需要通过直接访问数据链路层来实现。
大多数 ICMP 数据包的一个拷贝会传递给匹配的原始套接字,因此可以使用原始套接字来查看 ICMP 数据包。
所有内核不能识别的协议类型的 IP 数据包都传送给匹配的原始套接字。内核只会进行必要的检验。
在将一个 IP 数据包传送给原始套接字之前,内核需要选择匹配的原始套接字,这需要满足以下条件:
数据包的协议域必须与接收原始套接字的协议类型匹配。
如果原始套接字调用了 bind
函数绑定了本地 IP 地址,则到达的 IP 数据包的源 IP 地址必须与绑定的本地 IP 地址相匹配。
如果原始套接字调用了 connect
函数指定了对方的 IP 地址,则到达的 IP 数据包的源 IP 地址必须与指定的对方 IP 地址相同。
Microsoft公司以Berkeley Sockets规范为范例,定义了Windows Socktes 规范,简称Winsock规范。这是Windows操作系统环境下的套接字网络应用程序编程接口(API)。
Winsock以库函数的方式实现。不仅包括了Berkeley Sockets风格的库函数,也包括了一组针对Windows操作系统的扩展库函数,使编程者能充分利用Windows操作系统的消息驱动机制进行编程。
阻塞与非阻塞
菜没好,要不要死等->数据就绪前要不要等待?
阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。非阻塞遇到这些情况,都是直接返回。
同步与异步
菜好了,谁端->数据就绪后,数据操作谁完成?
数据就绪后需要自己去读是同步,数据就绪直接读好再回调给程序是异步。
同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(AIO) | |
---|---|---|---|---|
客户端个数:I/O线程 | 1:1 | M : N ( 其 中 M 可以大于N) | "M:1(1个I/O线程处理多个客户端连接) | M:0(不需要启动额外的1/O线程,被动回调) |
I/O类型(阻塞) | 阻塞I/O | 阻塞I/O | 非阻塞I/O | 非阻塞I/O |
I/O类型(同步) | 同步I/O | 同步I/O | 同步I/O(I/O多路复用) | 异步I/O |
API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
Windows Sockets 规范是一套开放的、支持多种协议的Windows下的网络编程接口。目前, Windows Sockets从1.0版发展到了2.2版,已成为Windows网络编程的事实上的标准。
1.Windows Sockets 1.1版本
在Winsock.h包含文件中,定义了所有WinSock 1.1版本库函数的语法、相关的符号常量和数据结构。库函数的实现在WINSOCK.DLL动态链接库文件中。
(1)WinSock 1.1 全面继承了Berkeley Sockets规范,见表 3.1
(2)数据库函数 表3.2列出了Winsock规范定义的数据库查询例程。其中六个采用getXbyY()的形式,大多要借助网络上的数据库来获得信息。 getXbyY()函数都返回一个指针,指向某种数据结构区域,这些结构区域是由Winsock实现分配的,用来放置函数返回的数据信息。
要注意:这些指针指向的数据结构数据是易失的,它们只在该线程的下一个WinSock API调用之前有效。在一个线程中,只有一份这个结构的副本。因此,应该在发出下一个WinSock API调用之前把所需的信息复制出来。
(3)WinSock 1.1 扩充了Berkeley Sockets规范 针对微软 Windows的特点,WinSock 1.1定义了一批新的库函数,提供了对于消息驱动机制的支持,有效地利用Windows多任务多线程的机制。见表3.3
这些扩充主要是提供了一些异步函数,并增加了符合Windows消息驱动特性的网络事件异步选择机制。
(4)WinSock 1.1只支持TCP/IP协议栈
2.WinSock 2.2
WinSock 2.2在源码和二进制代码方面与WinSock 1.1兼容,WinSock 2.2增强了许多功能。
(1)支持多种协议:通过SPI( Service Provider Interface )接口支持其他协议,例如AppleTalk、Novell与Xerox等。
(2)使用事件对象异步通知
(3)引入了重叠I/O的概念:增强I/O吞吐量与提高性能。
(4)服务的质量(QOS):例如使用WSAConnect函数提出连接请求时可以指定所要求的QoS。
注: 重叠 I/O是WIN32的一项技术,可以要求操作系统为你传送数据,并且在传送完毕时通知你。这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。
(5)套接口组:允许应用程序通知底层的服务提供者一组特定的套接口是相关的。套接口组的特性包括了组内单个套接口之间的相关特性和整个组的服务规范的特性。
(6)扩展的字节顺序转换例程:可以针对不同的协议需求进行多种字节序的转换,具体的字节顺序由PROTOCL_INFO结构指明。
(7)分散/聚集方式I/O:可以在单次系统调用中对多个缓冲区输入输出的方法,可以把多个缓冲区的数据写到单个数据流,也可以把单个数据流读到多个缓冲区中。这种输入输出方法也称为向量 I/O。与之不同,标准读写系统调用(read,write)可以称为线性I/O(linear I/O)。
(8)新增了许多函数。
3.WinSocket 1.1中的阻塞问题
阻塞是在把应用程序从Berkeley套接口环境中移植到Windows环境中的一个主要焦点。
在Berkeley套接口模型中,一个套接口的操作的缺省行为是阻塞方式的,除非程序员显式地请求该操作为非阻塞方式。
在Windows环境下,推荐程序员在尽可能的情况下使用非阻塞方式(异步方式)的操作。因为非阻塞方式的操作能够更好地在Windows环境下工作。
Windows早期的版本(3.1版本)是非抢先的多任务环境,即若一个程序不主动放弃其控制权,别的程序就不能执行。因此在设计Windows Sockets 程序时,尽管系统支持阻塞操作,但还是反对程序员使用该操作。但由于 SUN 公司下的 Berkeley Sockets 的套接字默认操作是阻塞的,WINDOWS 作为移植的 SOCKETS 也不可避免对这个操作支持。
Windows Sockets为了实现当一个应用程序的套接字调用处于阻塞时,能够放弃CPU让其它应用程序运行,它在调用处于阻塞时便进入一个叫“HOOK”的例程,此例程负责接收和分配WINDOWS消息,使得其它应用程序仍然能够接收到自己的消息。
在Windows Sockets实现中,对于不能立即完成的阻塞操作做如下处理:DLL初始化→循环操作。在循环中,它可以处理任何 WINDOWS 消息,并检查这个Windows Sockets调用是否完成(WSAIsBlocking函数),在必要时,它可以放弃CPU让其它应用程序执行。可以调用WSACancelBlockingCall函数取消此阻塞操作。
一个阻塞操作尚未完成之前,除了 WSACancelBlockingCall() 和WSAIsBlocking() 函数,不允许应用程序再调用其他任何WinSock函数,如果某个WinSock函数在此时被调用,则会失败并返回错误代码WSAEINPROGRESS。
阻塞操作的循环处理步骤:
(1)调用 BlockingHook 函数 。
(2)检查使用者是否已调用 WSACancelBlockingCall() 取消了当前WinSock函数的调用
(3)检查当前WinSock函数的调用是否已完成了?
for(;;) { /* flush messages for good user response */ while(BlockingHook()) ; /* check for WSACancelBlockingCall()*/ if(operation_cancelled()) break; /* check to see if operation completed */ if(operation_complete()) break; /* normal completion */ }
缺省的BlockingHook()函数如下:
BOOL DefaultBlockingHook(void) {MSG msg;BOOL ret;/* get the next message if any */ret = (BOOL)PeekMessage(&msg,NULL,0,0,PM_REMOVE);/* if we got one,process it */if (ret) {TranslateMessage(&msg);DispatchMessage(&msg);}/* TRUE if we got a message */return ret;}
在 Windows Socket 中,阻塞处理例程(阻塞钩子函数) BlockingHook() 简单地获取并发送WINDOWS 消息。如果要对复杂程序进行处理,Windows Sockets 中还有WSASetBlockingHook() 提供用户安装自己的阻塞处理例程能力;与该函数相对应的则是WSAUnhookBlockingHook(),它用于删除先前安装的任何阻塞处理例程,并重新安装默认的处理例程。
WSASetBlockingHook()
:建立应用程式指定的 blocking hook 函数。
格式:
FARPROC PASCAL FAR WSASetBlockingHook( FARPROC lpBlockFunc)
参数:lpBlockfunc
指向要装设的 blocking hook
函式的位址
传回值: 指向前一个blocking hook
函式的位址
说明: 此函式可以设定自己的 Blocking Hook
函数。被设定的函数将会在应用程序呼叫到“blocking”动作时执行。唯一可在指定的 blocking hook 函数中调用的 Winsock 函数只有WSACancelBlockingCall()
。
假设设计了一个 Blocking Hook 函数叫myblockinghook()
,那么在程序中向 Winsock 系统注册的方法如下:(其中_hInst
代表此 task 的 Instance)
FARPROC lpmybkhook = NULL;lpmybkhook = MakeProcInstance( (FARPROC)myblockinghook, _hInst) );WSASetBlockingHook( (FARPROC)lpmybkhook );
在设定自己的 Blocking Hook
函数后,仍可以利用WSAUnhookBlockingHook()
函数来取消设定的 Blocking Hook
函数,而变更回原先系统默认的Blocking Hook
函数。
WSAUnhookBlockingHook()
:复原系统预设的 blocking hook 函数。
格式:
int PASCAL FAR WSAUnhookBlockingHook( void )
参数: 无
传回值: 成功 - 0
失败 - SOCKET_ERROR (通过WSAGetLastError()
可得知原因)
说明: 此函数取消使用者设定的 blocking hook函数,而恢复系统原先预设的 blocking hook函数。
注意:设计自己的阻塞处理例程时,除了函数WSACancelBlockingHook()
之外,它不能使用其它的 Windows Sockets API函数。在处理例程中调用WSACancelBlockingHook()
函数将取消处于阻塞的操作,它将结束阻塞循环。
只有在以下条件为真的时候,Windows Sockets DLL才调用阻塞钩子函数:历程是被定义为可以阻塞的,指定的套接口也是阻塞套接口,而且请求不能立刻完成。
说明:一个应用程式所设定的 Blocking Hook 函式,只会被这个应用程式所使用;其他的应用程式并不会执行到您设定的 Blocking Hook 函式的。
1.套接口数据类型和该类型的错误返回值
在UNIX中,包括套接口句柄在内的所有句柄,都是非负的短整数,在WinSock规范中定义了一个新的数据类型,称作SOCKET,用来代表套接字描述符。
typedef u_int SOCKET;
SOCKET可以取从0到INVALID_SOCKET-1之间的任意值。
在socket()例程和accept()例程返回时,检查是否有错误发生就不应该再使用把返回值和-1比较的方法,或判断返回值是否为负(这两种方法在BSD中都是很普通,很合法的途径)。取而代之的是,一个应用程序应该使用常量INVALID_SOCKET,该常量已在WINSOCK.H中定义。
例如:
典型的BSD风格:s = socket(...);if (s == -1) /* of s<0 */{...}更优良的风格:s = socket(...);if (s == INVALID_SOCKET){...}
2.select()
函数和FD_*
宏
select函数的实现有一些变化,每一组套接口仍然用fd_set类型代表,但它并不是一个位掩码。整个组的套接口是用了一个套接口数组来实现的。
typedef struct fd_set{ u_int fd_count; // how many are SET SOCKET fd_array[FD_SETSIZE]; // an array of SOCKETs,默认容量为64} fd_set;
3.错误代码的获得
在UNIX 套接字规范中,如果函数执行时发生了错误,会把错误代码放到errno
或h_errno
变量中。
在Winsock中,错误代码统一使用WSAGetLastError()
调用得到。
例如:典型的BSD风格:r = recv(...);if (r == -1&& errno == EWOULDBLOCK){...}更优良的风格:r = recv(...);if (r == -1 && WSAGetLastError() == EWOULDBLOCK){...} 虽然为了兼容性原因,错误常量与4.3BSD所提供的一致;应用程序应该尽可能地使用“WSA”系列错误代码定义。例如,一个更准确的上面程序片断的版本应该是:r = recv(...);if (r == -1&& WSAGetLastError() == WSAEWOULDBLOCK){...}
4.指针
所有应用程序与Windows Sockets使用的指针都必须是FAR
指针。为了方便应用程序开发者使用,Windows Sockets规范定义了数据类型LPHOSTENT
。
在DOS下(实模式)地址是分段的,每一段的长度为64K字节,刚好是16位(二进制的十六位)。
near指针的长度是16位的,所以可指向的地址范围是64K字节,通常说near指针的寻址范围是64K。
far指针的长度是32位,含有一个16位的基地址和16位的偏移量,将基地址乘以16后再与偏移量相加(所以实际上far指针是20位的长度)即可得到far指针的1M字节的偏移量。所以far指针的寻址范围是1M字节,超过了一个段64K的容量。
例如:一个far指针的段地址为0x7000,偏移量为0x1244,则该指针指向地址0x71224。如果一个far指针的段地址是0x7122,偏移量为0x0004,则该指针也指向地址0x71224。
在DOS下,如果没有指定一个指针是near或far,那么默认是near。所以far指针要显式指定。在Win32的保护模式默认的是far指针。
什么时候使用far指针?
当使用小代码或小数据存储模式(small)时,不能编译一个有很多代码或数据的程序。因为在64K的一个段中,不能放下所有的代码与数据。为了解决这个问题,需要指定以far函数或far指针来使用这部分的空间(64K以外的空间)。
5.重命名的函数
(1)close()改变为closesocket()
(2)ioctl()改变为ioctlsocket()
6.Winsock select函数支持的最大套接口数目
在WINSOCK.H中缺省值是64,在编译时由常量FD_SETSIZE
决定。
7.头文件
一个Windows Sockets应用程序只需简单地包含winsock2.h就足够了。
8.支持原始套接口
9.Winsock规范对于消息驱动机制的支持
体现在异步选择机制、异步请求函数、阻塞处理方法、错误处理、启动和终止等方面。
WSAStartup初始化Windows_Sockets_API
应用程序
WSACleanup释放所使用的Windows_Sockets_DLL
1.初始化函数WSAStartup()
Winsock 应用程序要做的第一件事,就是必须首先调用WSAStartup()函数对Winsock进行初始化。初始化也称为注册。注册成功后,才能调用其他的Winsock API函数。
(1)WSAStartup()函数的调用格式
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData );
参数wVersionRequested
:指定应用程序所要使用的WinSock规范的版本。主版本号在低字节,副版本号在高字节。
参数lpWSAData
:是一个指向WSADATA结构变量的指针,用来返回WinSock API实现的细节信息
(2)WSAStartup()函数的初始化过程
第一步:检查系统中是否有一个或多个Windows Sockets实现的实例。
第二步:检查找到的WinSock实现是否可用,主要是确认WinSock实现的版本号。
第三步:建立WinSock实现与应用程序的联系。
第四步:函数成功返回时,会在lpWSAData所指向的WSADATA结构中返回相关信息。
(3)WSADATA结构的定义
// 定义常量 WSADESCRIPTION_LEN 为 256,用于描述缓冲区的最大长度#define WSADESCRIPTION_LEN 256// 定义常量 WSASYS_STATUS_LEN 为 128,用于系统状态缓冲区的最大长度#define WSASYS_STATUS_LEN 128// 定义结构体 WSADATA,用于存储关于Windows套接字库的信息typedef struct WSAData {WORD wVersion; // 存储WinSock版本号的低字WORD wHighVersion; // 存储WinSock版本号的高字char szDescription[WSADESCRIPTION_LEN + 1]; // 存储描述信息的缓冲区,加1是为了存储字符串结束符'