Winsock 网络编程知识
Winsock是Windows下网络编程的基础。
1.Winsock的初始化与释放
Version: Requires Windows Sockets 2.0.
Header: Declared in Winsock2.h.
Library: Use Ws2_32.lib.
在使用Winsock相关函数时需要对Winsock库进行初始化,而在使用完成后需要对Winsock库进行释放。完成Winsock库的初始化和释放的函数如下
- 初始化
int WSAStartup(
WORD wVersionRequested,// 需要初始化的版本
LPWSADATA lpWSAData// 指向WSADATA的指针
);
返回0说明调用成功,调用失败,返回其他值
The Windows Sockets WSAStartup function initiates use of Ws2_32.dll by a process.
- 释放函数
int WSACleanup (void)
代码如下
#include "stdafx.h"
int main()
{
WORD wVersionRequested;
WSADATA wsadata;
int err;
wVersionRequested = MAKEWORD( 2, 2 );
/*
WORD MAKEWORD(
这个宏创建一个无符号16位整形,通过连接两个给定的无符号参数
返回值:一个无符号16位整形数。
BYTE bLow, //指定新变量的低字节序;
BYTE bHigh //指定新变量的高字节序;
);
*/
err = WSAStartup(wVersionRequested, &wsadata);
/*
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
*/
if (err != 0)
{
return -1;
}
if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion != 2))
{
/* Tell the user that we could not find a usable */
/* WinSock DLL. */
WSACleanup();
return -1;
}
WSACleanup();
return 0;
}
2.套接字的创建与关闭
套接字用于根据指定的协议类型来分配一个套接字描述符。该描述符主要用在客户端和服务器端进行通信连接,当套接字使用完毕时应该关闭套接字以释放资源。创建套接字与关闭套接字的函数为socket() 和closesocket()
socket
SOCKET socket(
int af;
int type;
int protocol;
);
af:
指定协议族 AF_INET/PF_INET 2
AF表示地址族,PF表示协议族
在window两者相同,在linux两者不同,一般使用PF_INET
type:
指定新套接字描述符的类型。这里有三个:SOCK_STREAM、SOCK_DGRAM和SOCK_RAW,分别表示流套接字、数据包套接字和原始协议借口。
protocol:
指定应用程序所使用的通信协议
IPPROTO_TCP、IPPRPTP_UDP、IPPROTO_ICMP等协议
这个参数的值根据第2个参数的值进行选择。
第2个参数如果使用SOCK_STREAM,那么第3个参数应该使用IPPROTO_TCP
第2个参数如果使用SOCK_DGRAM,那么第3个参数应该使用IPPROTO_UDP
即第二个参数是SOCK_STREAM/SOCK_DGRAM - 第三个可以默认0
如果第二个参数是SOCK_RAW,那么第三个参数就必须指定,不能用0值
The Windows Sockets socket function creates a socket that is bound to a specific service provider.
返回值:一个新的套接字描述,失败返回INVALID_SOCKET,WSAGetLastError()函数获得错误码。
关闭套接字
int closesocket(SOCKET s);
3.面向连接协议的函数
面向连接的函数:bind(), listen(), accept(), connect(), send()和recv()。这些函数是常用的面向连接的函数,只是一个基础。Winsock库的函数非常多
通过socket()函数可以创建一个新的套接字描述符,但是它只是一个描述符,为网络的一些资源做准备。要真正在网络通信需要本地的地址与端口信息。本地地址与端口号信息要去套接字描述符进行关联,进行绑定。在Winsock函数中,使用bind()函数完成套接字与地址端口信息的绑定。
bind()函数定义如下:
int bind(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);
该函数有三个参数,
第1个参数s是新创建的套接字描述符,也就是用socket()函数创建的描述符,
第2个参数name是一个sockaddr的结构体,提供套接字一个地址和端口信息,
第3个参数namelen是sockaddr结构体的大小
sockaddr结构体定义如下
struct sockaddr {
u_short sa_family; // address family
char sa_data[14]; // up to 14 bytes of direct address
}
该结构体共有16字节,在该结构体之前使用的协议为sockaddr_in,该结构体的定义如下:
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sockaddr结构体是为了保持各个特定协议之间的兼容性而设计的。为bind()函数指定地址和端口时,向sockaddr_in结构体填充相应的内容, 而调用函数时应该使用sockaddr结构体。
在sockaddr_in结构体中,还有一个结构体in_addr,该结构体在winsock2.h中的定义如下:
struct in_addr {
union {
struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { u_short s_w1,s_w2; } S_un_w;
u_long S_addr;
} S_un;
};
该结构体中是一个共用体S_un,包含两个结构体变量和一个u_long类型变量。一般使用的IP地址是使用点分十进制表示,而in_addr结构体中却没有提供用来保存点分十进制表示IP地址的数据类型,这时需要使用转换函数,把点分十进制表示的IP地址转换成in_addr结构体可以接受的类型,这里使用的转换函数是inet_addr(),该函数入下:
unsigned long inet_addr(
const char FAR *cp
);
该函数是将点分十进制表示IP地址转换成unsigned long类型的数值。
该函数的参数cp是指向点分十进制IP地址字符指针。同时该函数有一个逆函数,是将unsigned long型的数值型IP地址转换为点分十进制的IP地址,该函数如下:
char FAR * inet_ntoa(struct in_addr in);
sockaddr_in结构体中的sin_port表示端口,这个端口需要使用大尾方式字节序存储(大尾方式和小尾方式是两种不同的存储方式。)在intel X86 架构下,数值存储方式默认都是小尾方式字节序,而TCP/IP的数值存储方式都是大尾方式的字节序。为了方便转换,winsock2.h中提供了htons()和htonl()两个函数,并且提供了他们的逆函数ntohs()和ntohl()。
host > net 主机字节序转换为网络字节序
u_short htons(u_short hostshort);
u_long htonl(u_long hostlong);
net > host 网络字节序转换为主机字节序
u_short ntohs(u_short netshort);
u_long ntohl(u_long netlong);
在有些结构系统下,主机字节序和网络字节序是相同的,那样函数不进行任何转换。
// 创建字节序
SOCKET sLisent = socket(PF_INET, SOCK_STREAM, IPPOROTO_TCP)
struct socketaddr_in ServerAddr;
ServerAddr.sin_family = AF_INET;
ServerAddr.sin_addr.S_un.S_addr = inet_addr("10.10.30.12");
ServerAddr.sin_port = htons(1234);
bind(sLisent, (SOCKADDR*)&ServerAddr, sizeof(ServerAddr));
对于服务器端的地址可以指定为INADDR_ANY宏,表示“任意地址”。当客户端发起连接时,服务器操作系统受到客户端的连接,根据网络的配置情况会自动选择一个IP地址和客户端进行通信。
当套接字与地址端口信息绑定以后,就需要让端口进行监听,当端口处于监听状态以后就可以接受其他主机的连接了。监听端口和接受连接请求的函数分别为listen()和accept()。
监听端口的函数定义如下:
int listen(SOCKET s, int backlog);
s: 监听的套接字描述符,
backlog:允许进入请求连接队列的个数
在winsock2.h中,最大值为SOMAXCONN,
#define SOMAXCONN 0x7fffffff
接受连接请求的函数定义如下:
SOCKET accept(SOCKET s, struct sockaddr FAR *addr, int FAR *addrlen);
第1个参数s是处于监听的套接字描述符
第2个参数name指向sockaddr结构体的指针,返回客户端的地址信息
第3个参数namelen是指sockaddr结构体的大小
该函数从连接请求队列中获得连接信息,创建新的套接字描述符,获取客户端地址。新创建的套接字用于和客户端进行通信。
上面为面向服务器端的函数,完成了一系列服务器应有的基本的动作
- bind()函数将套接字描述符与地址信息进行绑定
- listen()函数将绑定过套接字描述置于监听状态
- accept()函数获取连接队列中的连接信息,创建新的套接字描述符,以便与客户端通信
面向连接的客户端只需完成与服务器的连接这样一个动作就可以实现和服务器端的通信了。创建套接字描述符后,使用connect()函数就可以完成与服务器的连接。
connect()函数的定义如下
int connect(SOCKET s, const struct sockaddr FAR *name, int namelen);
该函数的作用是将套接字进行连接,当客户端使用connect()函数与服务器连接后,客户端和服务器端就可以进行通信了。通信时主要就是信息的发送和信息的接受。分别是send()和recv()。
发送函数send()定义如下
int send(SOCKET s, const char FAR *buf, int len, int flags);
s: accept()函数返回的套接字描述符
buf: 是发送消息的缓冲区
len: 是缓冲区的长度
flags: 通常赋值0
接受recv()定义如下
int recv(SOCKET s, char FAR *buf, int len, int flags);
4.非面向连接协议的函数
在面向连接的TCP协议中,服务器端将套接字描述符与地址进行绑定后,需要将端口进行监听,等待接受客户端的连接请求,而在客户端则需要连接服务器,完成这些步骤保证面向连接的TCP协议的可靠传输,在调用connect()函数的过程中也完成了TCP的“三次握手”。非面向连接的UDP协议在开发上基本与面向连接TCP的协议相同。在非面向连接的UDP协议开发中,服务器端不需要对端口进行监听,也就不需要等待客户端的连接请求,而客户端也不需要完成与服务器端的连接。中间的"三次握手"过程也就省去了,这样UDP协议相对于TCP协议来讲就显得不可靠了,但在效率方面却要快于TCP协议。
在非面向连接协议的开发中,服务器端不再需要调用listen()、accept()函数,客户端不再需要调用connect()函数。而服务器和客户端的通信换为sendto()和recvfrom()函数即可。
sendto()函数定义如下
int sendto(
SOCKET s,
const char FAR *buf,
int len,
int flags,
const struct sockaddr FAR *to,
int tolen
);
s: 套接字描述符
buf: 是要发送数据的缓冲区
len: 是buf的长度
flags:0
to:指向sockaddr结构体的指针
tolen:第五个参数的长度
该函数是用来在UDP协议通信双方进行发送数据的函数
recvfrom()函数的定义如下
int recvfrom(
SOCKET s,
char FAR* buf,
int len,
int flags,
struct sockaddr FAR *from,
int FAR *fromlen
);
该函数是用来在UDP协议通信双方进行接收数据的函数。该函数的用法与sendto()相同