第四章网络应用程序工作机制
4.1 客户端-服务器模型
每一个网络应用程序都有一个通信端点。有两种类型的端点:客户端和服务器。在初始阶段,首先启动服务器,等待接收信息;客户端启动后发送第一个数据包。在经过初始阶段的联系后,无论客户端还是服务器都能够正常发送和接收数据了。此后客户端和服务器的相对关系可以在任意时刻改变。
客户端和服务器的关联
客户端和服务器要进行通信,那么对于它们的socket来说,必须是同一种socket类型。两个socket要么都是流(TCP)类型,要么都是数据报(UDP)类型。
客户端应用程序必须能够定位和识别一个服务器的socket,服务器应用程序通过对自己的socket命名来建立起身份识别,这样客户端才能够引用它。一个TCP/IP的socket名称包含IP地址、端口号以及协议。客户端能够通过WindowsSockets的服务名函数找到服务器的标准端口号,而且,如果知道服务器的主机名,利用WindowsSockets的主机名解析函数,可以很容易地找到服务器的IP地址。
当客户端socket成功联系上一个服务器端的socket时,两者的socket名称构成一个关联,一个socket与另一个绑定并构成关联后,此关联将建立两个socket的识别号。这时,通过在关联中自己和对方名称的结合,就能够唯一地识别每一个socket。一个关联包含5个元素:
· 协议(客户端和服务器socket都采用同样的协议)
· 客户端IP地址
· 客户端端口号
· 服务器IP地址
· 服务器端口号
对于流类型,关联的生存期直接与客户端和服务器之间的TCP虚电路的建立和撤销相关。而数据报由于是无连接的,关联的生存期并不确定。在理论上,每一个数据报的传输都将建立和撤销一个关联。但实际上,大多数UDP应用程序在整个socket生存期中都使用同一个关联。
关联中包含的信息为网络中的每个数据包提供识别和引导功能,是他们得以从一个网络应用程序到达对等方的应用程序。
4.2 网络程序概览
所有的网络应用程序都可以缩简为5个简单的步骤:
· 打开socket
· 命名socket
· 与另一个socket建立关联
· 在socket之间发送和接收数据
· 关闭socket
上述的5个步骤与文件的I/O操作非常类似,打开文件操作对应于前三个操作。在文件的I/O操作过程中,文件名是永久存在的,而socket的名称在关闭socket之后就消失了。
4.3 socket的打开
Socket是通信的端点,它在软件中创建,相当于计算机的网络接口。
客户端和服务器都需要通过socket来访问网络,打开socket的API函数为socket()函数:
SOCKET PASCAL FAR socket ( intaf, /* protocol suite */
int type, /*protocol type */
int protocol /* protocol name *);
参数说明:
af:“地址簇”,否则视为socket域。
type:socket类型
protocol:采用的协议
打开socket的操作返回一个描述符。这个描述符和与Windows的句柄类似。永远不要为socket描述符预先设定任何特定值。唯一的非法值有WinSock宏INVALID_SOCKET定义。type参数常常隐式的指示地址族中的protocol。例如在TCP/IP地址族中,SOCK_DGRAM类型的socket所用协议总是UDP,SOCK_STREAM类型的socket所用协议总是TCP。在这种情况下,socket()函数总是忽略protocol参数的值。因此,把它设为0是一个好习惯。
Socket()函数调用成功时,返回一个socket描述符;调用失败时,返回一个INVALID_SOCKET值。在调用失败后,需要调用WSAGetLastError()检索对应的错误值,提示用户失败的原因。
服务器应用程序必须对socket进行命名,如果没有命名,协议栈将拒绝客户端的通信请求。构成socket名称需要有三个属性:协议、端口号和IP地址。服务器必须为socket分配这3个属性,客户端必须引用这3个属性,客户端和服务器之间才能通信。
4.4 socket的命名
4.4.1 sockaddr结构
struct sockaddr{
<span style="white-space:pre"> </span>u_short sa_family; // address family
<span style="white-space:pre"> </span>char sa_data[14]; // undefined
};
sa_family:地址族
sa_data:根据地址族的值定义的地址结构数据区
上述为socket地址结构的一般形式。这种结构是不可用的。通常需要进行重定义。
4.4.2 sockaddr_in结构
struct sockaddr_in {
<span style="white-space:pre"> </span>short sin_family; // address family(PF_INET)
<span style="white-space:pre"> </span>u_short sin_port; // port (service)number (16-bit)
<span style="white-space:pre"> </span>struct in_addr sin_addr; // IP address (32-bit)
<span style="white-space:pre"> </span>char sin_zero[8]; // <unused filler>
};
sin_family:地址族
sin_prot:按网络顺序的16位的端口号。
sin_addr: 按网络顺序的32位的因特网地址。
4.4.3 端口号
sin_port域的值就是端口号,这个无符号的16位的值用于表示服务器所采用的应用层协议。
0~1023:为知名服务保留
1024:IANA保留
1025~5000:用户自定义服务的典型范围
4.4.4 本地IP地址
sin_addr的取值IP地址,根据具体函数的不同,可以是本地IP地址,也可以是远程IP地址。调用bind()函数时,sin_addr总是指向本地IP地址,也就是使用调用bind()函数的主机的接口地址来初始化sin_addr。在大多数系统中,我们可以简单的使用INADDR_ANY来要求协议栈自动指派本地IP地址。
4.4.5 什么是socket名称
socket名称包含三个要素:协议、端口和IP地址。
bind()函数用sockaddr_in结构中的值命名本地socket。
int PASCAL FAR bind (
socket s, //an unbound socket
Struct sockaddr FAR *addr, // local port and IP addr
int namelen // addr structure length
);
s:socket句柄
addr:指向socket地址结构的指针
namelen:addr所指向的socket结构的长度sizeof(struct sockaddr)
4.4.6 客户端socket名称是可选的
客户端的socket不要求对其命名。也很少有应用程序这么要求。
4.5 与另一个socket建立关联
构建一个关联需要结合两端的socket名称:
· 服务器为建立关联做准备 listen()
· 客户端初始化这个关联 connect()
· 服务器完成关联 accept()
每一个关联都是唯一的。关联的唯一性决定了网络数据包的唯一性。它引导每个数据包在网络中的传输。
4.5.1 服务器如何准备建立关联
一个数据报(UDP)服务器在接收数据时建立关联,只要调用WSAAsyncSelect()、recv()、recvfrom()或者select()中的任何一个,即可为接收数据作好准备。一个流(TCP)服务器,还需要调用listen()函数来准备建立关联。
int PASCAL FAR listen (
SOCKET s, //a named, unconnected socket
int backlog); // pending connect queue length
s:已命名的socket句柄(已调用bind()函数),但尚未连接
backlog:等待连接的队列长度(不等于已接纳并建立连接的数目)。
backlog参数的含义是:当对服务器已经接纳的连接进行处理时,希望在栈中排队的到达的连接请求的数目。
listen()函数调用成功返回0值,失败时返回SOCKET_ERROR。这个函数的调用通常是不会失败的。如果失败,最可能的错误时由函数WSAAsyncLastError()返回WSAEMFILE(10024),表明无可用的socket描述符。这种情况通过减少backlog的值可能会有帮助。
在调用了listen()函数之后需要调用另一个函数来检测来自客户端的连接请求,这个函数可以是:accept()、select()或WSAAsyncSelect()。accept()函数最简单,但效率不高。在比较复杂的服务器程序中通常使用select()函数。WSAAsyncSlect()函数可被任何Windows Sockets应用程序选用。
4.5.2 客户端如何发起一个关联
connect()函数在一个TCPsocket上发起创建虚电路,或者为UDP socket设置一个默认的socket名称。
流(TCP)客户端创建关联必须发起一个链接,通过connect()函数实现。
int PASCAL PAR connect (
SOCKET s, // an unconnceted socket
struct sockaddr FAR *addr, // remote port and IP addr
int namelen); // addr structure length
s:socket句柄
addr:socket地址结构指针(对TCP/IP,总是采用sockaddr_in结构)
namelen:addr所指向的socket结构的长度(sizeof(struct sockaddr))
在调用connect()函数之前,进行初始化socket地址结构时,所使用的sin_addr和sin_port是远程socket名称的。
connect()函数调用成功时返回0值,失败时返回SOCKET_ERROR,对于TCP socket,connect()调用失败后,WSAAsyncLastError()函数返回的最常见的错误信息是WSAECONNREFUSED(10061).引起此错误的只有这几种情况:服务器没有运行、你在客户端(或服务器端)对sin_port的初始化不正确或者IP地址不正确。对于UDPsocket,最常见的错误是WSAEADDRINUSE(10048),如果在同一个本地socket山,调用一次以上connect()函数,并且引用同一个远程socket名称时,就会产生这个错误。
4.5.3 服务器如何完成一个关联
对于数据报(UDP)服务器,完成一个关联和UDP客户端发起一个关联一样简单。服务器简单地使用recv()函数或者recvfrom()函数读取客户端发送给它的数据。
流(TCP)服务器要完成关联的建立,可以通过调用:accept()函数或者select()函数指示监听socket上有可写数据(writefds标志集),或者当接收到WSAAsyncSelect()的Windows消息FD_ACCEPT。采用accept()方式,在函数调用成功时,关联就已经建立,其余两种方式还需要在调用accept()函数来完成关联的建立。
SOCKET PASCAL FAR accept(
SOCKET s, // a listening socket
struct sockaddr FAR *addr, // name of incoming socket
int FAR *addrlen); //length of sockaddr
s: socket句柄
addr:socket地址结构指针(对TCP/IP,总是采用sockaddr_in结构)
addrlen:addr所指向的socket结构的长度(sizeof(structsockaddr))
在调用accept()函数之前不许要初始化socket结构,这与另外两个函数connect()和bind()不同。在accept()函数调用成功后会返回一个新的有效的socket,失败时返回INVALID_SOCKET,不过该函数极少失败。
4.6 socket之间的发送与接收
4.6.1 在“已连接的”socket山发送数据
int PASCAL FAR send(
SOCKET S, // associated socket
sonst char FAR *buf, // buffer with outgoing data
int len, //bytes to send
int flags); //option flags
s:socket句柄
buf:指向缓存区的指针,缓存区包含应用程序要发送的数据
len:发送数据的长度(以字节为单位)
flags:影响发送的标志位(MSG_OOB,MSG_DONTROUTE)
在调用send()函数之前,必须先调用函数connect()函数建立关联,UDP需要得到有效的目标socket名称,而TCP需要建立虚电路。如果不先调用connect函数,send()函数会调用失败,调用WSAGetLastError()函数将返回WSAENOTCONN错误。
参数buf 指向要发送的数据的第一个字节。参数len的值设为缓冲区中要发送数据的字节数。对于UDP socket这个字节数不超过数据报的最大长度限制。而在TCP socket中可以使用任何值,但函数并不能保证再一次调用中发送所有请求数据。参数flags 是可选的,通常将它设为0.
调用send()函数成功后返回发送的字节数,失败时返回SOCKET_ERROR。对于TCP socket而言,即使发送字节数少于请求的字节数,操作仍将成功返回。而对于UDP socket而言,如果数据报过大,那么操作将失败,调用WSAGetLastError()函数将返回WSAEMSGSIZE(10040)错误。
此外,send()函数的成功返回并不一定说明数据已经发送到网络中,只说明协议栈有足够的空间缓存数据。协议栈可能会为了遵循协议的约定推迟传输。
4.6.2 在“无连接的”socket上发送数据
int PASCAL FAR sendto (SOCKET s, // a valid socket
const char FAR *buf, // buffer with outgoing data
int len, // bytes to send
int flags, // option flags
struct sockaddr FAR *to, // remote socket name
int tolen); // length of sockaddr
s:socket句柄
buf:指向缓存区的指针,缓存区包含应用程序要发送的数据
len:发送数据的长度(以字节为单位)
flags:影响发送的标志位(MSG_OOB,MSG_DONTROUTE)
to:指向包含目的地址含端口号(socket名称)的socket结构的指针(对TCP/IP,总是采用sockaddr_in结构)
tolen:to指针所指向的socket结构的长度(sizeof(structsockaddr))
4.6.3 接收数据
int PASCAL FAR recv (SOCKET s, // associated socket
char FAR *buf, // buffer with incoming data
int len, // bytes to recv
int flags); // option flags
s:socket句柄
buf:指向缓存区的指针,缓存区包含应用程序要发送的数据
len:发送数据的长度(以字节为单位)
flags:影响发送的标志位(MSG_OOB,MSG_DONTROUTE)
int PASCAL FAR sendto (SOCKET s, // a valid socket
const char FAR *buf, //buffer with outgoing data
int len, // bytes to recv
int flags, // option flags
struct sockaddr FAR *to, // remote socket name
int tolen); // length of sockaddr
s:socket句柄
buf:指向接收数据缓存区的指针
len:接收数据的长度(以字节为单位)
flags:影响接收函数的标志位(MSG_OOB,MSG_DONTROUTE)
to:指向包含源地址含端口号(socket名称)的socket结构的指针(对TCP/IP,总是采用sockaddr_in结构)
tolen:to指针所指向的socket结构的长度(sizeof(structsockaddr))
4.6.4 socket解复用器中的关联
4.7 socket的关闭
int PASCAL FAR closesocket(SOCKET s) //a valid socket
closesocket()函数调用成功时返回0值失败时返回SOCKET_ERROR。在closesokcet()函数调用成功返回后,任何引用该socket描述符的函数调用都将失败,并指示WSAENOTSOCK(10038)错误。
对于数据报(UDP)socket,closesocket()函数的任务只是将socket的资源归还给协议栈。函数调用总是立即返回。对于流(TCP)socket,在向协议栈归还socket资源的同时,还要关闭连接。
int PASCAL FAR shutdown (
SOCKET s, // avalid socket
int how); // flag describing shutdown
how:指明行为方式的标志。有三个可选值:0:禁止接收(不影响低层协议);1:禁止发送(发送TCP FIN保温到远端socket);2:禁止发送和接收(发送TCP FIN报文到远端socket)。
4.8 客户端和服务器端概览
1. 面向连接的(TCP)应用程序
客户端 | 服务端 |
socket() | socket() |
用服务器(远程)的socket名称初始化sockeaddr_in结构 | 用服务器(本地)的socket名称初始化sockaddr_in结构 |
| bind() |
| Listen() |
connect()——————————————> |
|
| accept() |
<创建了关联,双方均可以发送和接收 | |
send()————————————————> | recv() |
recv() <—————————————— | send() |
closesocket() | closesocket()(已连接的socket) |
| closesocket()(监听socket) |
2. 无连接的(UDP)应用程序
2.1 一次性设置远程socket一次性
客户端 | 服务端 |
socket() | socket() |
用服务器(远程)的socket名称初始化sockeaddr_in结构 | 用服务器(本地)的socket名称初始化sockaddr_in结构 |
| bind() |
connect()——————————————> | recv() |
<创建了关联,双方均可以发送和接收 | |
recv() <—————————————— | send() |
closesocket() | closesocket()(已连接的socket) |
2.2 每个数据报均设置远程socket名称
客户端 | 服务端 |
socket() | socket() |
用服务器(远程)的socket名称初始化sockeaddr_in结构 | 用服务器(本地)的socket名称初始化sockaddr_in结构 |
| bind() |
sendto()————————————————> | recvfrom() |
recvfrom() <—————————————— | sendto() |
closesocket() | closesocket()(已连接的socket) |