这一小节介绍网络编程。首先我们介绍一下计算机网络的基本知识,然后着重介绍一下Windows Socket程序的编写。
首先,介绍几个基本概念。什么是计算机网络?它是相互连接的独立自主的计算机的集合。它们是如何通信的呢,需要一个东西来表明我要跟哪个计算机进行通信,在网络上,为每个计算机分配了一个“IP”地址,通过地址来找到想要通信的计算机。具体的通信是计算机的某个程序实现的,一台计算机可能同时有多个程序在使用网络。为了区分它们,为每个程序提供了一个“端口号”来标识自己。具体通信时所发送的内容,成为协议,它规定了我们发送的格式:除了发送的内容以外,还包括这些东西是谁发送的,要发给谁,总共有多长等等。而信息具体是如何从一台主机,发送到另一台主机,则会有很大的不确定性:
不同的通信媒介:是通过有线传输的,还是无线网络?
不同的操作系统:Unix、Windows
不同的应用环境:移动、固定
不同的业务类型:对时延的要求、对差错控制的要求。
等等,使得实际中的相互通信的网络异常复杂,如何解决这个问题呢?国际标准化组织(ISO)提出了OSI(Open System Interconnected)七层参考模型,将网络按照不同的功能划分为7层:
应用层
表示层
会话层
传输层
网络层
物理层:提供二进制传输,确定在通信信道上如何传输比特流。一条物理信道上所能传送的信息的最快速度是有限制的,不是我们想传送多块就能传送多快的。为了对抗复杂的传输环境(这一点在无线通信中尤其明显),物理层通常要使用非常复杂的调制、编码技术,来在在一定差错容忍度的前提下,尽可能的多发送。
数据链路层:提供介质访问,增强物理层的传输功能,建立一条无差错的传输线路。比如对于收到物理层发送过来的数据,需要通过确认请求或者简单的差错控制编码(比如奇偶效验)来判断这一帧数据是否有错误,如果错误了,则通知发送发重新发送。
网络层:IP寻址和路由。因为网络上的数据可以经由多条路线到达目的地,所以其中要考虑路由算法、拥塞控制等问题。
传输层:为源端主机到目的端主机提供可靠的数据传输服务,隔离网络的上下层协议,是得网络应用于下层协议无关。也就是应用程序与应用程序之间的连接。
会话层:两个相互通信的应用进程之间建立、组织和协调其相互之间的通信。比如电影里使用对讲机时,一句话说完后总要加一句“over”。
表示层:被传送的数据如何表示。
应用层:用户所提供的服务。
要注意,这7层模型是功能上的划分,并不是具体一定要有这七层。
下面介绍一下应用层、传输层和网络层的常见协议(这是笔试题中常考的):
应用层协议:远程登录协议(Telnet)、文件传输协议(FTP)、超文本传输协议(HTTP)、域名服务(DNS)、简单哟见传输协议(SMTP)、邮局协议(POP3)等。
传输层:传输控制协议(TCP)、用户数据报协议(UDP)。
这两个协议值得仔细说说。
TCP协议是面向连接的,也就是说,在双方通信之前,已经安排好了一条通信线路(不管它具体是什么)共他俩使用,别人不能使用,等他俩通信结束后,需要释放这条线路。TCP是通过3次握手建立的:
1.客户端给服务器发送SYN(syn = j)包,进入SYN_SEND状态。
2.服务器接收到SYN包,确认客户的SYN(ack = j+1),同时自己也发送一个SYN包(syn = k),把它俩都发送出去,服务器进入SYN_RECV状态。
3.客户端收到服务器的SYN+ACK包,向服务器发送ACK(ack = k+1)。客户端和服务器都进入ESTABLISHED状态。此时,连接已经建立完毕,可以发送消息了。
上面只是正常的建立连接过程,其中的任何一步都有可能失败,至于失败以后的操作,这里就不细说了。
下面再看看UDP协议:这是一种无连接的、不可靠的协议。这意味着可能向对方发送的消息时对方无法接到。或者,向一个根本不存在的IP地址或者端口发送消息。既然是不可靠的,为什么还要使用它呢?因为UDP协议不需要建立连接,没有数据重传,所以实时性较高。比如我们看视频时,一两个像素的错误我们根本不会发觉。
网络层:网际协议(IP),Internet互联网报文控制协议(ICMP)、Internet组管理协议IGMP。
传输层
网络层
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机、服务器模式:客户向服务器发送请求,服务器收到请求后,提供相应的服务。为什么这么设计呢?首先,建立网络的原因是因为网络中软硬件资源、运算能力和信息分布不均,需要共享,从而拥有资源多的主机提供服务,资源少的客户请求服务。其次,网间进程通信完全是异步的,相互通信的进程间即不存在父子关系,又不存在共享的内存缓冲区,需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步。
它们的通信过程如下:
服务器先运行:
1.打开一个通信通道并告知本地主机,他愿意在某一地址和端口上接收客户的请求。
2.等待客户请求到达端口。
3.接收到重复服务请求没处理该请求并发送应答信号。收到并发服务的请求,要激活一个新的进程或者线程来处理这个客户请求。新的进程或者线程处理此客户的请求,并不需要对其他请求作出相应。等服务完成后,关闭新进程与客户的通信链路,并终止。
4.返回第二步,等待另一请求。
5.关闭服务器。
客户端:
1.打开一个通信通道并连接到服务器所在的主机的特定端口。
2.向服务器发送服务请求报文,等待接收应答,继续提出请求。
3.请求结束后关闭通信通道并终止。
讲了这么多,我们可以隐约感觉到,网络编程是一件很麻烦的事情,为了方便的开发应用软件程序,美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统套接字(socket),是得程序员可以很方便的访问TCP/IP协议,从而开发各种网络应用程序。后来,socket又引入到windows操作系统。我们先介绍与之相关的函数,然后给出几个例子:
1.使用WSAStartup进行版本协商。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested指明了版本号。高字节是副版本,低字节是主版本,可以通过MAEWORD宏来获得。lpWSAData是用来返回值的,这个函数会把它加载的版本信息填到这个结构里面。具体的使用方法可以参照MSDN给出的例子。
2.使用WSACleanup()来释放为应用程序分配的资源,与WSAStartup相对应。
3.使用socket函数来创建套接字:
SOCKET socket( int af, int type,int protocol );
af参数是地址族,对于TCP/IP协议,它只能是AF_INET。
type指明了socket的类型,对于1.1版本的socket,他只接受两种类型:SOCK_STREAM、SOCK_DGRAM
protocol指明了与特定地址家族相关的协议,如果为0,则根据地址格式和套接字类别,自动选择一个合适的协议。
如果函数调用成功,则会返回一个SOCK数据类型的套接字描述符:如果调用失败,则返回一个INVALID_SOCKET值。
4.使用bind函数将套接字绑定到本地的某个地址和端口上:
int bind( SOCKET s, const struct sockaddr FAR *name,int namelen);
s指定要绑定的套接字,name是一个指向sockaddr结构体类型的指针,这个结构体表明了本地信息。
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
由于这个结构是为所有地址族准备的,所以不同的协议会有一定的区别,所以用第三个参数指明结构的长度。
再回过头来看第二个参数,第一成员指明了地址族,第二个成员是14个字节的内存区域,对于不同的协议,有不同的内容。对于TCP/IP协议,使用sockaddr_in 结构来替换sockaddr结构:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family指明了地址族,应使用AF_INET;sin_port指明了端口号;sin_addr指明了主机的IP地址;sin_zero则只是为了填充字节,是得sockaddr_in 与sockaddr长度相同。其中sin_addr又是一个结构体,定义如下:
typedef struct in_addr
{
union
{
struct
{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct
{
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR
这个结构其实是一个联合体,通常我们都是将点分十进制的IP地址转换为u_long类型,并赋值给S_addr成员。
一般情况下,我们可以使用INADDR_ANY允许套接字向任何分配本地机器的IP地址发送或者接收数据。这个参数的实际意义在于,一般情况下,一台主机只有一个IP地址,但如果主机有两个网卡,那么他会有两个IP地址,如果我们只想使用其中的一个供套接字使用,我们可以使用inet_addr函数将本地的IP地址(点分十进制字符串形式),转化为unsigned long并赋给S_addr;与之相反的转化为inet_ntoa函数,将S_addr转化为点分十进制,供打印输出使用。
5.listen函数将指定的套接字设置为监听模式。
int listen( SOCKET s, int backlog );
s 为指定的套接字;backlog为等待队列的最大长度。
6.使用accept函数来接受客户端的连接请求
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
s为已被设置为监听模式的套接字;addr为指向客户端的sockaddr的地址,通过函数来获取值;addrlen为地址长度。
6.通过send函数发送消息
int send(SOCKET s,const char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf为要发送消息的地址,len为消息长度,flags一般设为0即可。
7.通过recv函数获取消息:
int recv(SOCKET s,char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf用来保存接收的数据,len表示缓冲区的长度,flags一般填0即可。
8.使用connect函数与特定的套接字连接
int connect( SOCKET s,const struct sockaddr FAR *name,int namelen);
s为即将建立连接的那个套接字;name指定了服务器端的地址信息,s为地址信息长度。
9.使用recvfrom接受消息
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR *from,int FAR *fromlen);
s为准备接受数据的套接字,buf为接收数据的缓冲区,len为缓冲区的长度,flag一般填0,from指针用来存储发送方的地址信息,fromlen为地址的长度。
10.使用sendto向一个特定的目的方发送数据。
int sendto( SOCKET s,const char FAR *buf,int len,int flags,const struct sockaddr FAR *to,int tolen );
s为套接字,buf为发送的数据的地址,len为数据的长度,flags一般为0,to指针指向目标套接字的地址,tolen为地址的长度
11.字节序的转换函数。
首先先搞清楚什么是字节序。一般情况下,我们使用电脑都是低位在前、高位在后的,这被称为小端字节序;而网络传输时使用的是低位在前,高位在后的大端字节序。如果不进行转化一个16为数据0X1234就被网络认为是0X3412了。转换的函数有两个:
u_short htons( u_short hostshort );将一个16位数转换为网络字节序
u_long htonl( u_long hostlong );将一个32位数转换为网络字节序
介绍完函数,我们就先举一个利用TCP协议编写的简单的网络通信的例子。我们先看一下基本步骤:
服务器端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.将套接字设为监听状态(listen)。
4.接受客户端的发送请求(accept)。
5.发送或者接收数据(send/recv)。
6.关闭套接字(closesocket),一次通信结束。
7.转4.
客户端端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.连接到服务器(connect)。
4.发送或者接收消息(send/recv)。
5.关闭套接字(closesocket)。
6.释放资源(WSACleanup)。
首先,介绍几个基本概念。什么是计算机网络?它是相互连接的独立自主的计算机的集合。它们是如何通信的呢,需要一个东西来表明我要跟哪个计算机进行通信,在网络上,为每个计算机分配了一个“IP”地址,通过地址来找到想要通信的计算机。具体的通信是计算机的某个程序实现的,一台计算机可能同时有多个程序在使用网络。为了区分它们,为每个程序提供了一个“端口号”来标识自己。具体通信时所发送的内容,成为协议,它规定了我们发送的格式:除了发送的内容以外,还包括这些东西是谁发送的,要发给谁,总共有多长等等。而信息具体是如何从一台主机,发送到另一台主机,则会有很大的不确定性:
不同的通信媒介:是通过有线传输的,还是无线网络?
不同的操作系统:Unix、Windows
不同的应用环境:移动、固定
不同的业务类型:对时延的要求、对差错控制的要求。
等等,使得实际中的相互通信的网络异常复杂,如何解决这个问题呢?国际标准化组织(ISO)提出了OSI(Open System Interconnected)七层参考模型,将网络按照不同的功能划分为7层:
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
物理层:提供二进制传输,确定在通信信道上如何传输比特流。一条物理信道上所能传送的信息的最快速度是有限制的,不是我们想传送多块就能传送多快的。为了对抗复杂的传输环境(这一点在无线通信中尤其明显),物理层通常要使用非常复杂的调制、编码技术,来在在一定差错容忍度的前提下,尽可能的多发送。
数据链路层:提供介质访问,增强物理层的传输功能,建立一条无差错的传输线路。比如对于收到物理层发送过来的数据,需要通过确认请求或者简单的差错控制编码(比如奇偶效验)来判断这一帧数据是否有错误,如果错误了,则通知发送发重新发送。
网络层:IP寻址和路由。因为网络上的数据可以经由多条路线到达目的地,所以其中要考虑路由算法、拥塞控制等问题。
传输层:为源端主机到目的端主机提供可靠的数据传输服务,隔离网络的上下层协议,是得网络应用于下层协议无关。也就是应用程序与应用程序之间的连接。
会话层:两个相互通信的应用进程之间建立、组织和协调其相互之间的通信。比如电影里使用对讲机时,一句话说完后总要加一句“over”。
表示层:被传送的数据如何表示。
应用层:用户所提供的服务。
要注意,这7层模型是功能上的划分,并不是具体一定要有这七层。
下面介绍一下应用层、传输层和网络层的常见协议(这是笔试题中常考的):
应用层协议:远程登录协议(Telnet)、文件传输协议(FTP)、超文本传输协议(HTTP)、域名服务(DNS)、简单哟见传输协议(SMTP)、邮局协议(POP3)等。
传输层:传输控制协议(TCP)、用户数据报协议(UDP)。
这两个协议值得仔细说说。
TCP协议是面向连接的,也就是说,在双方通信之前,已经安排好了一条通信线路(不管它具体是什么)共他俩使用,别人不能使用,等他俩通信结束后,需要释放这条线路。TCP是通过3次握手建立的:
1.客户端给服务器发送SYN(syn = j)包,进入SYN_SEND状态。
2.服务器接收到SYN包,确认客户的SYN(ack = j+1),同时自己也发送一个SYN包(syn = k),把它俩都发送出去,服务器进入SYN_RECV状态。
3.客户端收到服务器的SYN+ACK包,向服务器发送ACK(ack = k+1)。客户端和服务器都进入ESTABLISHED状态。此时,连接已经建立完毕,可以发送消息了。
上面只是正常的建立连接过程,其中的任何一步都有可能失败,至于失败以后的操作,这里就不细说了。
下面再看看UDP协议:这是一种无连接的、不可靠的协议。这意味着可能向对方发送的消息时对方无法接到。或者,向一个根本不存在的IP地址或者端口发送消息。既然是不可靠的,为什么还要使用它呢?因为UDP协议不需要建立连接,没有数据重传,所以实时性较高。比如我们看视频时,一两个像素的错误我们根本不会发觉。
网络层:网际协议(IP),Internet互联网报文控制协议(ICMP)、Internet组管理协议IGMP。
由于7层模型在使用起来很不方便,实际中应用更广泛的是TCP/IP模型:
传输层
网络层
网络接口层
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户机、服务器模式:客户向服务器发送请求,服务器收到请求后,提供相应的服务。为什么这么设计呢?首先,建立网络的原因是因为网络中软硬件资源、运算能力和信息分布不均,需要共享,从而拥有资源多的主机提供服务,资源少的客户请求服务。其次,网间进程通信完全是异步的,相互通信的进程间即不存在父子关系,又不存在共享的内存缓冲区,需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步。
它们的通信过程如下:
服务器先运行:
1.打开一个通信通道并告知本地主机,他愿意在某一地址和端口上接收客户的请求。
2.等待客户请求到达端口。
3.接收到重复服务请求没处理该请求并发送应答信号。收到并发服务的请求,要激活一个新的进程或者线程来处理这个客户请求。新的进程或者线程处理此客户的请求,并不需要对其他请求作出相应。等服务完成后,关闭新进程与客户的通信链路,并终止。
4.返回第二步,等待另一请求。
5.关闭服务器。
客户端:
1.打开一个通信通道并连接到服务器所在的主机的特定端口。
2.向服务器发送服务请求报文,等待接收应答,继续提出请求。
3.请求结束后关闭通信通道并终止。
讲了这么多,我们可以隐约感觉到,网络编程是一件很麻烦的事情,为了方便的开发应用软件程序,美国伯克利大学在UNIX上推出了一种应用程序访问通信协议的操作系统套接字(socket),是得程序员可以很方便的访问TCP/IP协议,从而开发各种网络应用程序。后来,socket又引入到windows操作系统。我们先介绍与之相关的函数,然后给出几个例子:
1.使用WSAStartup进行版本协商。
int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);
其中wVersionRequested指明了版本号。高字节是副版本,低字节是主版本,可以通过MAEWORD宏来获得。lpWSAData是用来返回值的,这个函数会把它加载的版本信息填到这个结构里面。具体的使用方法可以参照MSDN给出的例子。
2.使用WSACleanup()来释放为应用程序分配的资源,与WSAStartup相对应。
3.使用socket函数来创建套接字:
SOCKET socket( int af, int type,int protocol );
af参数是地址族,对于TCP/IP协议,它只能是AF_INET。
type指明了socket的类型,对于1.1版本的socket,他只接受两种类型:SOCK_STREAM、SOCK_DGRAM
protocol指明了与特定地址家族相关的协议,如果为0,则根据地址格式和套接字类别,自动选择一个合适的协议。
如果函数调用成功,则会返回一个SOCK数据类型的套接字描述符:如果调用失败,则返回一个INVALID_SOCKET值。
4.使用bind函数将套接字绑定到本地的某个地址和端口上:
int bind( SOCKET s, const struct sockaddr FAR *name,int namelen);
s指定要绑定的套接字,name是一个指向sockaddr结构体类型的指针,这个结构体表明了本地信息。
struct sockaddr
{
u_short sa_family;
char sa_data[14];
};
由于这个结构是为所有地址族准备的,所以不同的协议会有一定的区别,所以用第三个参数指明结构的长度。
再回过头来看第二个参数,第一成员指明了地址族,第二个成员是14个字节的内存区域,对于不同的协议,有不同的内容。对于TCP/IP协议,使用sockaddr_in 结构来替换sockaddr结构:
struct sockaddr_in
{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family指明了地址族,应使用AF_INET;sin_port指明了端口号;sin_addr指明了主机的IP地址;sin_zero则只是为了填充字节,是得sockaddr_in 与sockaddr长度相同。其中sin_addr又是一个结构体,定义如下:
typedef struct in_addr
{
union
{
struct
{
unsigned char s_b1,s_b2,s_b3,s_b4;
} S_un_b;
struct
{
unsigned short s_w1,s_w2;
} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR
这个结构其实是一个联合体,通常我们都是将点分十进制的IP地址转换为u_long类型,并赋值给S_addr成员。
一般情况下,我们可以使用INADDR_ANY允许套接字向任何分配本地机器的IP地址发送或者接收数据。这个参数的实际意义在于,一般情况下,一台主机只有一个IP地址,但如果主机有两个网卡,那么他会有两个IP地址,如果我们只想使用其中的一个供套接字使用,我们可以使用inet_addr函数将本地的IP地址(点分十进制字符串形式),转化为unsigned long并赋给S_addr;与之相反的转化为inet_ntoa函数,将S_addr转化为点分十进制,供打印输出使用。
5.listen函数将指定的套接字设置为监听模式。
int listen( SOCKET s, int backlog );
s 为指定的套接字;backlog为等待队列的最大长度。
6.使用accept函数来接受客户端的连接请求
SOCKET accept( SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
s为已被设置为监听模式的套接字;addr为指向客户端的sockaddr的地址,通过函数来获取值;addrlen为地址长度。
6.通过send函数发送消息
int send(SOCKET s,const char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf为要发送消息的地址,len为消息长度,flags一般设为0即可。
7.通过recv函数获取消息:
int recv(SOCKET s,char FAR *buf,int len,int flags);
s为已建立连接的套接字,buf用来保存接收的数据,len表示缓冲区的长度,flags一般填0即可。
8.使用connect函数与特定的套接字连接
int connect( SOCKET s,const struct sockaddr FAR *name,int namelen);
s为即将建立连接的那个套接字;name指定了服务器端的地址信息,s为地址信息长度。
9.使用recvfrom接受消息
int recvfrom(SOCKET s,char FAR* buf,int len,int flags,struct sockaddr FAR *from,int FAR *fromlen);
s为准备接受数据的套接字,buf为接收数据的缓冲区,len为缓冲区的长度,flag一般填0,from指针用来存储发送方的地址信息,fromlen为地址的长度。
10.使用sendto向一个特定的目的方发送数据。
int sendto( SOCKET s,const char FAR *buf,int len,int flags,const struct sockaddr FAR *to,int tolen );
s为套接字,buf为发送的数据的地址,len为数据的长度,flags一般为0,to指针指向目标套接字的地址,tolen为地址的长度
11.字节序的转换函数。
首先先搞清楚什么是字节序。一般情况下,我们使用电脑都是低位在前、高位在后的,这被称为小端字节序;而网络传输时使用的是低位在前,高位在后的大端字节序。如果不进行转化一个16为数据0X1234就被网络认为是0X3412了。转换的函数有两个:
u_short htons( u_short hostshort );将一个16位数转换为网络字节序
u_long htonl( u_long hostlong );将一个32位数转换为网络字节序
介绍完函数,我们就先举一个利用TCP协议编写的简单的网络通信的例子。我们先看一下基本步骤:
服务器端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.将套接字设为监听状态(listen)。
4.接受客户端的发送请求(accept)。
5.发送或者接收数据(send/recv)。
6.关闭套接字(closesocket),一次通信结束。
7.转4.
客户端端:
1.进行版本协商(WSAStartup)。
2.创建一个套接字(socket)。
3.连接到服务器(connect)。
4.发送或者接收消息(send/recv)。
5.关闭套接字(closesocket)。
6.释放资源(WSACleanup)。