一、计算机网络基础知识
计算机网络是相互连接的独立自主的计算机的集合,最简单的网络形式是由两台计算机组成。
计算机A通过网络与计算机B通信,要完成一次通信,A主机需要知道与谁进行通信。
假如你正与张三进行通信,张三就是与你通信的人的名字。如果你的周围有许多人,你想要与张三进行通信,你就得说: "张三,我晚上请你吃饭。”这样的话,其他人听到这句话是不会有反应的。于是你就完成了与张三的这次通信。
在网络上,一台主机要与另一台主机进行通信,首先要知道与之通信的那台主机的名称,在Internet上通过一个称之为IP地址的4个字节的整数来标识网络设备,通常采用点分十进制的格式表示IP地址。有了IP地址,就等于主机有了自己的唯一的身份。
对A主机来说,它若与B进行通信,它可以把数据发送给具有IP地址为:192.168.0.10的主机。
对于B主机来说,它若与A进行通信,它可以将数据发送给IP地址为:192.168.0.118的主机。
这样,A和B就完成一次通信。
但是,通信的过程中还有一个问题:例如当你与一个美国人交流时,如果你说的是中文,而对方说的是英文,那么你们之间是无法正常交流的。在Internet上,两台主机要进行通信,它们也要遵循约定的规则。我们把这种规则称为协议。如果A主机和B主机采用同样的协议,它们之间就可以进行通信了。
现在身份也有了,通信的规则也有了,两台主机是否就可以完"成通信了呢?
要注意的是,计算机是没有生命的,真正完成计算机间通信的是在计算机上运行的网络应用程序。但是在一台计算机上可以同时运行多个程序。
例如,我们可以一边使用网络蚂蚁下载资料,一边还可以通过Realplay在线收看流媒体电影。那么发送给某个IP地址所标识的主机的数据,应该由哪个网络应用程序来接收呢?
于是,为了标识在计算机上运行的每一个网络通信程序,为它们分别分配一个端口号。
在发送数据时,除了指定接收数据的主机IP地址以外,还要指定端口号。这样,在指定IP地址的计算机上,将会由在指定端口号上等待数据的网络应用程序接收数据。
1. 1 IP地址
🖤IP网络中每台主机都必须有一个惟一的IP地址;
🖤IP地址是一个逻辑地址;
🖤因特网上的IP地址具有全球惟一性;
🖤 32位,4个字节,常用点分十进制的格式表示。
例如: 192.168.0.16,每个字节用一个十进制的整数来表示,用一个点(.)来分隔各字节。
1.2 协议
🖤为进行网络中的数据交换(通信)而建立的规则、标准或约定(=语义+语法+规则);
🖤不同层具有各自不同的协议。
1.3 网络的状况
🖤多种通信媒介——有线、无线…
🖤不同种类的设备——通用、专用…
🖤不同的操作系统——UNIX、 Window…
🖤不同的应用环境——固定、移动……
🖤 不同业务种类——分时、交互、实时……
🖤宝贵的投资和积累——有形、无形……
🖤用户业务的延续性——不允许出现大的跌宕起伏。
它们互相交织,形成了非常复杂的系统应用环境。
1.4 网络异质性问题的解决
🖤网络体系结构就是使这些用不同媒介连接起来的不同设备和网络系统在不同的应用环境下实现互操作性,并满足各种业务需求的一种粘合剂,它营造了一种“生存空间”——任何厂商的任何产品、以及任何技术只要遵守这个空间的行为规则,就能够在其中生存并发展。
🖤 网络体系结构解决异质性问题采用的是分层方法——把复杂的网络互联问题划分为若干个较小的、单一的问题,在不同层上予以解决。这就像我们在编程时把问题分解为很多小的模块来解决一样。
1.5 ISO/OSI 七层参考模型
ISO国际标准化组织提出了OSI七层参考模型, OSI (Open System Interconnection)参考模型将网络的不同功能划分为7层。
从低到高各层的功能分别如下所述:
◼ 物理层
提供二进制传输,确定在通信信道上如何传输比特流。
◼ 数据链路层
提供介质访问,加强物理层的传输功能,建立一条无差错的传输线路。
◼ 网络层
提供IP寻址和路由。因为在网络上数据可以经由多条线路到达目的地,网络层负责找出最佳的传输线路。
◼ 传输层
为源端主机到目的端主机提供可靠的数据传输服务,隔离网络的上下层协议,使得网络应用与下层协议无关。
◼ 会话层
在两个相互通信的应用进程之间建立、组织和协调其相互之间的通信。
◼ 表示层
处理被传送数据的表示问题,即信息的语法和语义。如有必要,可使用一种通用的数据表示格式,在多种数据表示之间进行转换。例如在日期、货币、数值等本地数据表示格式和标准数据表示格式之间进行转换,还有数据的加解密、压缩和解压缩等。
◼ 应用层
为用户的网络应用程序提供网络通信的服务。
1.6 数据封装
一台计算机要向另一台计算机发送数据,首先必须将该数据打包,打包的过程称为封装。封装就是在数据前面加上特定的协议头部。
例如:利用TCP协议传送数据时,当数据到达传输层时,就会加上TCP协议头,当该数据到达网络层时,在其前面还会加上IP协议头。
OSI参考模型中,对等层协议之间交换的信息单元统称为协议数据单元(PDU, ProtocolData Unit), OSI参考模型中每一层都要依靠下一层提供的服务。为了提供服务,下层把上层的PDU作为本层的数据封装,然后加入本层的头部(有的层还要加入尾部,例如数据链路层),头部的数据中含有完成数据传输所需的控制信息。我们在寄信时,要把信件放到信封中,当收信人收到这封信时,他要打开信封,取出信件。这种数据自上而下递交的过程实际上就是不断封装的过程。到达目的地后自下而上递交的过程就是不断拆封的过程。由此可知,在物理线路上传输的数据,其外面实际上被封装了多层“信封"。但是,某一层只能识别由对等层封装的“信封”,而对于被封装在“信封”内部的数据仅仅是拆封后将其提交给上层,本层不作任何处理。
1.7 TCP/IP模型
TCP/IP起源于美国国防部高级研究规划署(DARPA)的一项研究计划——实现若干台主机的相互通信。现在, TCP/IP已成为Internet上通信的工业标准。
因为OsI七层参考模型比较复杂,所以目前应用的比较多的是TCP/IP模型,该模型包括4个层次:
◼ 应用层
◼ 传输层
◼ 网络层
◼ 网络接口层
TCP/IP与OSI参考模型的对应关系:
1.8 端口
按照OSI七层模型的描述,传输层提供进程(也就是活动的应用程序)通信的能力。为了标识通信实体中进行通信的进程(应用程序), TCP/IP协议提出了协议端口(protocolport, i称端口)的概念。
端口是一种抽象的软件结构(包括一些数据结构和1O缓冲区)。应用程序通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应的进程所接收,相应进程发给传输层的数据都通过该端口输出。
端口用一个整数型标识符来表示,即端口号。端口号跟协议相关, TCP/P传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立。也就是说,基于TCP和UDP协议的不同的网络应用程序,它们可以拥有相同的端口号。端口使用一个16位的数字来表示,它的范围是0-65535, 1024以下的端口号保留给预定义的服务。例如: http使用80端口。我们在编写网络应用程序时,要为程序指定1024以上的端口号。
1.9 套接字
套接字(socket)的引入为了能够方便的开发网络应用软件,由美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统调用套接字(socket), socket的出现,使程序员可以很方便地访问TCPIP,从而开发各种网络应用的程序。随着Unix的应用推广,套接字在编写网络软件中得到了极大的普及。后来,套接字又被引进了Windows等操作系统,成为开发网络应用程序的非常有效快捷的工具。
套接字存在于通信区域中。通信区域也叫地址族,它是一个抽象的概念,主要用于将通过套接字通信的进程的共有特性综合在一起。套接字通常只与同一区域的套接字交换数据(也有可能跨区域通信,但这只在执行了某种转换进程后才能实现). Windows Sockets只支持一个通信区域:网际域(AF-INET),这个域被使用网际协议簇通信的进程使用。
1.10 网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(低位先存),有的机器在起始地址存放高位字节(高位先存),基于Intel的CPU,即我们常用的PC机采用的是低位先存。为保证数据的正确性,在网络协议中需要指定网络字节顺序,TCP/P协议使用16位整数和32位整数的高位先存格式。由于不同的计算机存放数据字节的顺序不同,这样发送方发送数据后,即使接收方接收到该数据,也有可能无法查看所接收到的数据。所以在网络中不同主机间进行通信时,要统一采用网络字节顺序。
二、Windows Sockets的实现
2.1 套接字的类型
◼ 流式套接字(SOCKSTREAM)
提供面向连接、可靠的数据传输服务,数据无差错、无重复的发送,且按发送顺序接收。流式套接字实际上是基于TCP协议实现的。
◼ 数据报式套接字(SOCK-DGRAM)
提供无连接服务。数据包以独立包形式发送,不提供无错保证,数据可能丢失或重复,并且接收顺序混乱。数据报式套接字实际上是基于UDP协议实现的。·
◼ 原始套接字(SOCK_RAW)
2.2 基于TCP (面向连接)的socket编程
基于TCP (面向连接)的socket编程的服务器端程序流程如下:
- 创建套接字(socket)。
- 将套接字绑定到一个本地地址和端口上(bind)。
- 将套接字设为监听模式,准备接收客户请求(listen)。
- 等待客户请求到来;当请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept)。
- 用返回的套接字和客户端进行通信(send/recv)面
- 返回,等待另一客户请求。
- 关闭套接字。
基于TCP (面向连接)的socket编程的客户端程序流程如下:
- 创建套接字(socket)。
- 向服务器发出连接请求(connect)
- 和服务器端进行通信(send/recv)
- 关闭套接字。
在服务器端,当调用accept函数时,程序就会等待,等待客户端调用connect函数发出连接请求,然后服务器端接受该请求,于是双方就建立了连接。之后,服务器端和客户端就可以利用send和recv函数进行通信了。读者应注意,在客户端并不需要调用bind函数。因为服务器需要接收客户端的请求,所以必须告诉本地主机它打算在哪个IP地址和哪个端口上等待客户请求,因此必须调用bind函数来实现这一功能。而对客户端来说,当它发起连接请求,服务器端接受该请求后,在服务器端就保存了该客户端的IP地址和端口的信息。这样,对服务器端来说,一旦建立连接之后,实际上它已经保存了客户端的IP地址和端口号的信息,因此就可以利用所返回的套接字调用send/recv函数与客户端进行通信。
2.3 基于UDP (面向无连接)的socket编程
服务器端也叫接收端,对于基于UDP (面向无连接)的套接字编程来说,它的服务器端和客户端这种概念不是很强化,我们也可以把服务器端,即先启动的一端称为接收端,发送数据的一端称为发送端,也称为客户端。
我们先看一下接收端程序的编写:
- 创建套接字(socket)。
- 将套接字绑定到一个本地地址和端口上(bind)
- 等待接收数据(recvfrom)
- 关闭套接字。
对于基于UDP的套接字编程,为什么仍然需要调用bind函数进行绑定呢?应注意,虽然面向无连接的socket编程无须建立连接,但是为了完成这次通信,对于接收端来说,它必须先启动以接收客户端发送的数据,因此接收端必须告诉主机它是在哪个地址和端口上等待数据的到来,也就是说,接收端(服务器端)必须调用bind函数将套接字绑定到一个本地地址和端口上。
对于客户端程序的编写非常简单:
- 创建套接字(socket)
- 向服务器发送数据(sendto
- 关闭套接字。
注意,在基于UDP的套接字编程时,利用的是sendto和recvfrom这两个函数实现数据的发送和接收,而基于TCP的套接字编程时,发送数据是调用send函数,接收数据调用recv函数。
套接字表示了通信的端点。我们利用套接字进行通信与利用电话机进行通信是一样的,套接字相当于电话机,IP地址相当于总机号码,而端口号则相当于分机。
三、相关函数
3.1 WSAStartup函数
在利用套接字编程时,第一步需要加载套接字库,通过WSAStartup 函数实现。
-
函数功能
加载套接字库并进行套接字库的版本协商,也就是确定将使用的socket版本。 -
函数声明
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
-
参数说明
参数 说明 wVersionRequested 用来指定准备加载的Winsock库的版本。高位字节指定所需要的Winsock库的副版本,而低位字节则是主版本。版本号是:2.1,其中2是主版本号,1就是副版本号。可以利用MAKEWORD(x,y)宏 方便的获得wVersion Request的正确值。 lpWSAData 这是一个返回值,指向WSADATA结构的指针,WSAStartup函数用其加载的库版本相关的信息填入这个结构体中。 -
WSADATA结构体
typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription [WSADESCRIPTION_LEN+1]; char szSystemStatus [WSASYS_STATUS_LEN+1]; unsigned short iMaxSockets; unsigned short iMaxudpDg; char FAR * lpVendorInfo; } WSADATA, *LPWSADATA;
参数 说明 wVersion 打算使用的Winsock版本 wHighVersion 容纳的是现有的Winsock库的最高版本。 szDescription,szSystemStatus 这两个字段由特定的Winsock实施方案设定,事实上并没有用。 iMaxSockets,iMaxUdpDg 不要使用这两个字段,它们是假定同时最多可打开多少套接字和数据报的最大长度。然而,要知道数据报的最大长度应该通过WSAEnumProtocols函数来查询协议信息。同时最多可打开套接字的数目不是固定的,很大程度上和可用物理内存的多少有关。 IpVendorInfo 是为Winsock实施方案有关的指定厂商信息预留的,任何一个Win32平台上都没有使用这个字段。 如果
Ws2_32.dll
或底层网络子系统没有被正确地初始化或没有被找到,WSAStartup函数
将返回WSASYSNOTREADY
。此外,这个函数允许你的应用程序协商使用某种版本的WinSock规范,如果请求的版本等于或高于WinSock动态库所支持的最低版本,WSAData的wVersion成员中将包含你的应用程序应该使用的版本,它是动态库所支持的最高版本与请求版本中较小的那个。反之如果请求的版本低于WinSock动态库所支持的最低版本, WSAStartup函数将返回WSAVERNOTSUPPORTED
,关于WSAStartup函数更详细的信息,请查阅MSDN中相关内容。对于每一个WSAStartup函数的成功调用(即成功加载WinSock动态库后),在最后都对应一个WSACleanUp调用,以便释放为该应用程序分配的资源,终止对WinSock动态库的使用。
3.2 socket函数
加载套接字后,就可以调用socket函数创建套接字了。
-
函数声明
SOCKET socket(int af, int type, int protocol);
-
参数说明
参数 说明 af 指定地址族,对于TCP/IP协议的套接字,它只能是AF_INET(也可以写成PF_INET) type 指定Socket类型,对于1.1版本的Socket,它只支持两种类型的套接字,SOCK_STREAM指定产生流式套接字,SOCK_DGRAM产生数据报套接字 protocol 是与特定的地址家族相关的协议,如果指定为0,那么系统就会根据地址格式和套接字类别自动选择一个合适的协议。这是推荐使用的一种选择协议的方法。 -
返回值
若socket函数调用成功,会返回一个新的SOCKET数据类型的套接字描述符;
若调用失败,这个函数就会返回一个INVALID_SOCKET值,错误信息可以通过WSAGetLastError函数返回。
3.3 bind函数
创建了套接字后,应该将该套接字绑定到本地的某个地址和端口上,这需要通过bind函数实现。
-
函数声明
int bind(SOCKET s,const struct sockaddr FAR *name,int namelen);
-
参数
参数 说明 s 指定要绑定的套接字 name 指定了该套接字的本地地址信息,这是一个指向sockaddr结构的指针变量,由于该地址结构是为所有的地址家族准备的,此结构可能随所使用的网络协议不同而不同。 namelen 指定该地址结构的长度。 -
sockaddr结构的定义
struct sockaddr{ u_short sa_family; char sa_data[14]; };
参数 说明 sa_family 地址家族,对于TCP/IP协议的套接字,必须设置为AF_INET sa_data 仅仅表示要求一块内存分配区,起到占位的作用,该区域中指定与协议相关的具体地址信息。 由于实际要求的只是内存区,所以对于不同的协议家族,用不同的结构来替换sockaddr。 除了
sa_family
外,sockaddr是按网络字节顺序表示的。在基于TCP/IP的socket编辑过程中,可以用sockaddr_in
结构替换sockaddr,以方便我们填写地址信息。 -
sockaddr_in结构的定义
struct sockaddr_in{ short sin_family; unsigned short sin_port; struct in_addr sin_addr; char sin_zero[8]; };
参数 说明 sin_family 地址家族,对于IP地址,sin_family成员将一直是AF_INET; sin_port 将要分配给套接字的端口; sin_addr 套接字的主机IP地址; sin_zero 只是一个填充数,以使sockaddr-in结构和sockaddr结构的长度一样; -
返回值
如果这个函数调用成功,它将返回0。
如果调用失败,这个函数就会返回一个SOCKET_ERROR
,错误信息可以通过WSAGetLastError函数返回。
另外,
sockaddr_in
结构中sin_addr
成员的类型是in_addr,该结构的定义如下所示:struct in_addr { union { struct { u_char s_b1,s_b2, s_b3,s_b4; } s_un_b; struct { u_short s_wl,sw2; } s_un_w; u_long s addr; }S_un;
可以看到,in_addr结构实际上是一个联合,通常利用这个结构将一个点分十进制格式的IP地址转换为u_long类型,并将结果赋给成员S_addr。
3.4 inet_addr和inet_ntoa函数
可以将IP地址指定为INADDR_ANY
,允许套接字向任何分配给本地机器的IP地址发送或接收数据。多数情况下,每个机器只有一个IP地址,但有的机器可能会有多个网卡,每个网卡都可以有自己的IP地址,用INADDR_ANY
可以简化应用程序的编写。将地址指定为INADDR_ANY
,将允许一个独立应用接受发自多个接口的回应。如果我们只想让套接字使用多个IP中的一个地址,就必须指定实际地址,要做到这一点,可以用inet_addr
函数来实现,该函数的原型声明如下所示:
unsigned long inet_addr ( const char FAR* cp);
inet_addr
函数需要一个字符串作为其参数,该字符串指定了以点分十进制格式表示的IP地址(例如192.168.0.16)。而且inet_addr
函数会返回一个适合分配给S_addr的u_long类型的数值。
inet_ntoa函数会完成相反的转换,它接受一个in_addr结构体类型的参数并返回一个以点分十进制格式表示的IP地址字符串。该函数的原型声明如下所示:
char FAR* inet_ntoa ( struct in_addr in );
3.5 listen函数
listen函数的作用是将指定的套接字设置为监听模式。
-
声明原形
int listen ( SOCKET s, int backlog );
-
参数
参数 说明 s 套接字描述符 backlog 等待连接队列的最大长度。如果设置为 SOMAXCONN
,那么下层的服务提供者将负责将这个套接字设置为最大的合理值。要注意的是,设置这个值是为了设置等待连接队列的最大长度,而不是在一个端口上同时可以进行连接的数目。
例如:
如果将backlog参数设置为2,当有3个请求同时到来时,前两个连接请求就会被放到等待请求连接队列中,然后由应用程序依次为这些请求服务,而第3个连接请求就被拒绝了。
3.6 accept函数
Windows Sockets 的accept 函数接受客户端发送的连接请求。
-
声明
SOCKET accept(SOCKET s, struct Sockaddr FAR* addr,int FAR* addrlen);
-
参数
参数 说明 s 套接字描述符,该套接字已经通过listen函数将其设置为监听状态 addr 指向一个缓冲区的指针,该缓冲区用来接收连接实体的地址,也就是当客户端向服务器发起连接,服务器端接受这个连接时,保存发起连接的这个客户端的IP地址信息和端口信息; addrlen 一个返回值,指向一个整型的指针,返回包含地址信息的长度。
3.7 send函数
Windows Sockets的send函数通过一个已建立连接的套接字发送数据。
-
原型声明
int send(SOCKET s,const char FAR *buf, int len,int flags);
-
参数
参数 说明 s 是一个已建立连接的套接字; buf 指向一个缓冲区,该缓冲区包含将要传递的数据; len 是缓冲区的长度; flags 设定的值将影响函数的行为,一般将其设置为0即可。
3.8 recv函数
Windows Sockets的recv函数从一个已连接的套接字接收数据。
-
声明
int recv (SOCKET s,char FAR* buf,int len,int flags);
-
参数
参数 说明 s 建立连接之后准备接收数据的那个套接字; buf 是一个指向缓冲区的指针,用来保存接收的数据; len 是缓冲区的长度; flags 与send函数的第四个参数类似,通过设置这个值可以影响这些函数调用的行为。
3.9 connect函数
Windows Sockets的connect函数将与一个特定的套接字建立连接。
-
原型声明
int connect(SOCKET S,const struct sockadar FAR* nane,int namelen);
-
参数
参数 说明 s 即将在其上建立连接的那个套接字; nane 设定连接的服务器端地址信息; namelen 指定服务器端地址的长度。
3.10 recvfrom函数
Windows Sockets的recvfrom函数将接收一个数据报信息并保存源地址。
-
原型声明
int recvfrom (SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR* from, int FAR* fromlen);
-
参数
参数 说明 s 是准备接收数据的套接字; buf 是一个指向缓冲区的指针,该缓冲区用来接收数据; len 缓冲区的长度; flags 与send函数的第四个参数类似,通过设置这个值可以影响这些函数调用的行为; from 是一个指向地址结构体的指针,主要是用来接收发送数据方的地址信息; fromlen 是一个整型的指针,并且它是一个in/out类型的参数,表明在调用前需要给它指定一个初始值,当函数调用之后,会通过这个参数返回一个值,该返回值是地址结构的大小。
3.11 sendto函数
Windows Sockets的sendto函数将向一个特定的目的方发送数据。
-
原型声明
int sendto(SOCKET s,const char FAR * buf,int len,int flags,const struct sockadar FAR* to, int tolen);
-
参数
参数 说明 s 一个(可能已建立连接)的套接字描述符; buf 是一个指向缓冲区的指针,该缓冲区用来接收数据; len 缓冲区的长度; flags 与send函数的第四个参数类似,通过设置这个值可以影响这些函数调用的行为; to 是一个可选的指针,指定目标套接字的地址 tolen 是参数to中指定的地址的长度。
3.12 htons和htonl函数
Windows Sockets的htons函数将把一个u_short
类型的值从主机字节顺序转换为TCP/IP网络字节顺序。原型声明:
u_short htons(u_short hostshort );
该函数只有一个参数: hostshort,是一个以主机字节顺序表示的16位数值。
与htons函数相类似的还有一个函数: htonl,该函数将把一个u_long
类型的值从主机字节顺序转换为TCP/IP网络字节顺序。其原型声明如下所示:
u_long htonl (u_long hostlong );
该函数只有一个参数: hostlong,是一个以主机字节顺序表示的32位数值。