!上接Winsock网络编程知识(上)!
sockaddr结构体是为了保持各个特定协议之间的兼容性而设计的。为bind()函数指定的地址和端口时,向sockaddr_in结构体填充相应的内容,而调用函数时应该使用sockaddr结构体。
在sockaddr_in结构体中,还有一个结构体in_addr,该结构体在winsock2.h中的定义如下:1
2
3
4
5
6
7struct in_addr{
union {
struct { u_char s_bl,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_wl,s_w2; } S_un_w;
u_long S_addr;
}S_un;
};
该结构体是一个共用体S_un,包含2个结构体变量和1u_long类型变量。一般使用的IP地址的数据类型是使用点分十进制
表示的,而in_addr结构体中却没有提供用来保存点分十进制表示IP地址的数据类型,只是需要使用转换函数,把点分十进制
表示的IP地址转换成in_addr结构体可以接受的类型。这里使用的转换函数是inet_addr(),该函数的定义如下:1
unsigned long inet_addr(const char FAR *cp);
该函数是将点分十进制表示的IP地址转换成unsigned long
类型的数值。该函数的参数cp是指向点分十进制IP地址的字符指针
。同时该函数也是一个逆函数
,是将unsigned long型的数值型IP地址转换成点分十进制的IP地址,该函数的定义如下:1
char FAR * inet_ntoa(struct in_addr in);
sockaddr_in结构体中的sin_port表示端口,这个端口需要使用大尾方式字节序存储(也称大端和小端,是两种不同的存储方式。)在intel X86架构下,数值存储方式默认W为小尾方式字节序,而TCP/IP的数值的存储方式都是大尾方式的字节序。为了实现方便的转换,winsock2.h中提供为了方便的函数,即htons()和htonl()两个函数,并且提供了他们的逆函数ntohs()和ntohl()。
htons()和htonl()函数的定义分别如下:1
2u_short htons(u_short hostshort);
u_long htonl(u_long hostlong);
ntohs()和ntohl()函数的定义分别如下:1
2u_short ntohs(u_short netshort);
u_long ntohl(u_long netlong);
这4个函数中,前两个函数是将主机字节序
转换成网络字节序
,后两个是将网络字节序
转换成主机字节序
。在有些架构系统下,主机字节序和网络字节序是相同的,那么转换函数不进行任何转换,但是为了代码的移植性,还是会进行转换函数的调用。
具体的bind()函数的使用方法如下:1
2
3
4
5
6
7
8
9
10//创建套接字
SOCKET sListen = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
//对sockaddr_in结构体填充地址、端口等信息
struct sockaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.S_un.S_addr = inet_addr("10.10.30.16");
ServerAddr.sin_port = htons(1234);
//绑定套接字与地址信息
bind(sLisent, (SOCKADDR *)&ServerAddr, sizeof(serverAddr));
PS:对于服务器的地址可以指定为INADDR_ANY宏,表示“任意地址”或者“所有地址”。当客户端发起连接时。服务器操作系统接收到客户端的连接,根据网络的配置情况会自动选择一个IP地址和客户端进行通信。
当套接字与地址端口信息绑定后,就需要让端口进行监听,当端口进行监听状态以后就可以接受其他主机的连接了。监听端口和接受连接请求的函数分别为listen()和accept()。
监听端口的函数定义如下:1
int listen(SOCKET s, int backlog);
该函数有两个参数,第1个参数s是指定要监听的套接字描述符,第2个参数是backlog是允许进入请求连接队列的个数,backlog的最大值由系统指定,在winsock2.h中,其最大值由SOMAXCONN表示,该值的定义如下:1
接受连接请求的函数定义如下:1
SOCKET accept(SOCKET s,struct sockaddr FAR *addr, int FAR *addrlen);
该函数从连接请求队列
中获取连接信息
,创建新的套接字描述符,获取客户端地址。新创建的套接字用于和客户端进行通信。该函数有3个参数,第1个参数s是处于监听套接字描述符
,第2个参数addr是一个指向sockaddr结构体的指针
,用来返回客户端的地址信息,第3个参数addrlen是一个指向int型的指针变量
,用来传入sockaddr结构体的大小。
上面介绍的是面向连接的服务器端的函数,完成了一系列服务器应有的基本动作,如下:
首先,bind()函数将套接字描述符与地址信息进行绑定;
其次,listen()函数将套接字描述符置于监听状态;
最后,accept()函数获取连接队列中的连接信息,创建新的套接字描述符,以便与客户端通信。
面向连接的客户端只需要完成与服务器的连接这样一个动作就可以实现和服务器的通信了。创建套接字描述符后,使用connect()函数就可以完成与服务器的连接。
connet函数的定义如下:1
int connet(SOCKES s, const struct sockaddr FAR *name,int namelen);
该函数的作用是将套接字进行连接。该函数有3个参数,第1个参数表示创建好的套接字描述符
,第2个参数name是指向sockaddr结构体的指针
,sockaddr结构体中保存了服务器的IP和端口号,第3个参数namelen是指定sockaddr结构体的长度
。
当客户端使用connect()函数与服务器连接后,客户端和服务器就可以通信了。通信时主要就是信息的发送和接收。这里介绍的函数有两个,分别是send()和recv()。
发送函数send()的定义如下:1
int recv(SOCKET s, const char FAR *buf, int len, int flags);
该函数有4个参数,第1个参数s
是accept()函数返回的套接字描述符,第二个参数buf
是发送消息的缓冲区,第3个参数len
是缓冲区的长度,第4个参数flags
通常赋值为0值。
接收函数recv()的定义如下:1
int recv(SOCKET s,char FAR *buf,int len, int flags);
该函数有4个参数,该函数的使用方法和send()函数的使用方法相同。这里不再介绍。
4.非面向连接协议的函数
在面向连接的TCP协议中,服务器端将套接字描述符与地址进行绑定后,需要将端口进行监听
,等待接受客户端的连接请求,而在客户端则需要连接服务器,完成这些步骤就可以保证面向连接的TCP协议的可靠传输,在调用connect()函数的过程中也完成了TCP的“三次握手”的过程。非面向连接的UDP协议在开发上基本与面向连接TCP相同。在非面向连接的UDP协议开发中服务器端不需要对端口进行监听,也就不需要等待接受客户端的连接请求,而客户端也不需要完成与服务器的连接。中间的“三次握手”过程也就省略了,这样UDP协议现对于TCP协议来讲就显得不可靠,但是效率会更高。(游戏方面一般采用DUP协议)
在非面向连接协议开发中,服务器端不再调用listen()、accept()函数,客户端不再需要调用connect()函数。而服务器和客户端的通信换为sendto()和recvfrom()函数即可。
sendto函数的定义如下:1
2
3
4
5
6
7
8int sendto(
SOCKET s,
const char FAR *buf,
int len,
int flags,
const struct sockaddr FAR *to,
int tolen
);
该函数是用来在UDP协议通信双方进行发送数据的函数,该函数有6个参数,第1个参数s是套接字描述符,第2个参数buf是要发送数据的缓冲区,第3个参数len是指定第2个参数的长度,第4个参数通常赋0值,第5个参数to是一个指向sockaddr结构体的指针,这里给出接收信息的地址信息,第6个参数tolen是指定第5个参数的长度。
recvfrom()函数的定义如下:1
2
3
4
5
6
7
8int recvfrom(
SOCKET s,
const char FAR *buf,
int len,
int flags,
const struct sockaddr FAR *from,
int FAR *fromlen
);
该函数是用来在UDP协议通信双方进行接收数据的函数。该函数的用法与sendto()相同,这里不再介绍。
PS:在缓冲区后设置数据的长度是为了防止缓冲区溢出!