Windows Sockets 网络编程——第四章 网络应用程序工作机制

第四章网络应用程序工作机制

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)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值