这个是基于Winsock2的一个程序设计,我使用的软件是VS2017,当然其他软件应该也可以编译运行demo,这篇博客主要写一些Winsock接口问题,主要包含winsock库的装入和释放,winsock的寻址方式和字节顺序(大端小端),winsock编程流程,典型流程图,TCP的服务端与客户端编程,UDP的服务端与客户端编程以及时间协议和网络对时demo。
所有源代码可以在我的主页里面寻找对应的压缩包下载。
Winsock 是一个真正的协议无关的接口
2.1 Winsock库 现在使用Winsock2 WS2_32.lib 库链接
2.1.1 Winsock库的装入和释放
int WSAStartup(
WORD wVersionRequested, //指定想要加载的winsock库版本,高字节是次版本号,低字节是主版本号。
LPWSADATA lpWSAData //指向WSADATA结构体的 指针 ,返回DLL库的详细信息。
);
wVersionRequested参数使用 宏 MAKEWORD(x,y) x是高字节,y是低字节。
LPWSADATA 是一个结构体,WSAStartup使用所加载库的版本信息填充。
typedef struct WSAData
{
WORD wVersion; //库文件建议应用程序使用的版本
WORD wHighVersion; //库文件支持的最高版本
char szDescription[WSADESCRIPTION_LEN+1] //库描述字符串
char szSystemStatus[WSASYS_STATUS_LEN+1] //系统状态字符串
unsigned short iMacSoskets; //同时支持最大套接字数量
}WSADATA,FAR*LPWSADATA;
WSAStartup 成功返回0,否则用 WSAGetLastErroe()获取错误信息,它是调用API函数里的GetLastError,获取的是最后发生错误的代码。
**每一个对WSAStartup的调用,必须对应一个WSACleanup释放Winsock库
int WSACleanup(void);
**链接WS2_32.lib库 用 #pragma comment(lib,"WS2_32");
-----------------------------------------------------------------------------
2.2 Winsock的寻址方式和字节顺序
winsock兼容多个协议,所以必须使用通用的寻址方式。TCP/IP使用IP地址和端口号指定一个地址,但是其他协议也许采用不通的协议。
struct sockaddr
{
u_short sa_family; //地址使用的地址家族。
char sa_data[14]; //存储的数据在不同的地址家族中可能不同。
};
在Winsock中,通过 SOCKADDR_IN 结构体指定IP和端口:
struct sockaddr_in
{
short sin_family; //地址家族(指定地址格式),因为AF_INET;
u_short sin_port; //端口号
struct in_addr sin_addr; //IP地址
char sin_zero[8]; //空字节,设置为0;
};
sin_port:指定了TCP/UDP通信服务端口号(16位)。端口号分为3个范围:
0~1023 : 由IANA管理,保留为公共的服务使用。 20/21 80
1024~49151: 普通用户注册的端口号,由IANA列出。
49152~65535: 是动态或者私有的算口号。
sin_addr域用来存储IP地址(32位),被定义一个联合来处理整个32位的值,两个16位部分单独分开。
struct in_addr{
union{
struct {u_char s_b1,s_b2,s_b3,s_b4} S_un_b; //'aa.bb.cc.dd',每个不能超过255
struct {u_short s_w1,s_w2} S_un_w;
u_long S——addr;
}S_un;
};
sin_zero:没有使用,是为了与SOCKADDR结构体大小相同蔡设置。
**可以用inet_addr函数将IP的点分十进制转换为二进制表示。
**inet_ntoa 是 inet_addr 的逆函数。
unsigned long inet_addr(const char *cp);
char *inet_ntoa(struct in_addr in);
字节顺序:
长度跨越多个字节的数据被存储的顺序。如:0x12345678,小端存储:0x78,0x56,0x34,0x12 ==> 不重要的字节首先存储。
大端顺序(大尾顺序):最重要的字节首先存储。
因为协议数据要在这些机器间传输,所以必须 选定其中一种方式作为标准,否则容易引起混淆。
TCP/IP统一使用大端传输,也称为 网络字节序。
在sockaddr_in中除了sin_family 外,其余的都采用网络字节序存储
u_short htons(u_short hostshort);
u_short ntohs();
u_long htonl();
u_long ntohl();
----------------------------------------------------------------------------------------
获取地址信息:
通常,主机上的接口被静态地指定一个IP地址, 169.254.0.0/16 范围内的地址
获取Mac地址:获取自己和LAN中的MAC地址
获取本机的MAC地址很容易,用函数 GetAdaptersInfo 即可。
DWORD GetAdaptersInfo(
PIP_ADAPTER_INFO pAdapterInfo, //指向一个缓冲区,来获取IP_ADAPTER_INFO结构的列表
PULONG pOutBufLen //指定缓存区的大小,如果大小不够,此参数返回大小
); //函数调用成功返回ERROR_SUCCESS;
IP_ADAPTER_INFO 结构包含了本地计算机上网络适配器的信息。
typedef struct _IP_ADAPTER_INFO{
struct _IP_ADAPTER_INFO *Next; //指向适配器列表中的下一个适配器(计算机可能有多个适配器)
DWORD comboIndex; //保留字段
char AdapterName[MAX_ADAPTER_NAME_LENGTH+4]; //适配器名称
char Description[MAX_ADAPTER_DESCRIPTION_LENGTH+4]; //对适配器的描述
UINT AddressLength; //MAC地址的长度 48位
BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]; //mac地址
DWORD Index; //适配器索引
UINT Type; //适配器类型,如MIB_IF_TYPE_ETHERNET等
UINT DhcpEnabled; //指定适配器是否使DHCP(动态主机配置)有效
PIP_ADDR_STRING CurrentIpAddress; //保留字段
IP_ADDR_STRING IpAddressList; //此适配器相关的IP列表
IP_ADDR_STRING GatewayList; //网关地址列表
IP_ADDR_STRING DhcpServer; //HDCP服务器
BOOL HaveWins; //指定此适配器是否使用WINS
IP_ADDR_STRING PrimaryWinsServer; //WINS服务器的主IP地址
IP_ADDR_STRING SecondaryWinsServer; //WINS服务器的第二IP地址
time_t LeaseObtained; //获取当前DHCP租用时间
time_t LeaseExpires; //当前DHCP租用期满的时间
}IP_ADAPTER_INFO,*PIP_ADAPTER_INFO;
--------------------------------------------------------------------------
2.3 流程:Winsock编程的一般步骤是比较固定的。
不管是TCP还是UDP,都先初始化sock;
class CinitSock
{
public:
CinitSock(BYTE minorVer = 2,BYTE majorVer = 2)
{
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer,majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
::WSAGetLastError();
}
}
~CinitSock()
{
::WSACleanup();
}
};
TCP:
1.套接字的创建和关闭
SOCKET socket(
int af, //指定套接字的使用的地址格式,winsock只支持AF_INIT
int type, //用来指定套接字类型
int protocol //配合type参数使用,用来指定使用的协议类型。可以是IPPROTO_TCP/UDP等,也可以是0
);
type参数用来指定套接字的类型。有流,数据报,原始三类。
SOCK_STREAM: 流套接字,使用TCP提供有连接的可靠传输。
SOCK_DGRAM: 数据报套接字,使用UDP提供无连接的不可靠传输。
SOCK_RAW: 原始套接字,Winsock接口不使用特定的协议去封装,由程序自行处理数据报以及协议首部。
失败返回-1(INVALID_SOCKET)
当不使用套接字时,应用closesocket关闭,成功返回0,失败返回SOCKET_ERROR;
int closesocket(SOCKET sockfd);
2.绑定指定的IP和端口号:绑定的是本地的
int bind(
SOCKET s, //
const struct sockaddr FAR * name, //
int namelen //
);
**inet_pton(AF_INET, "127.0.0.1", &add.sin_addr);和 inet_addr()功能一样。但VS2017要加
#include
3.设置套接字进入监听 仅在支持连接的套接字上使用
int listen(SOCKET sockfd,int backlog);
backlog:监听队列中允许保持的尚未处理的最大连接数量。
如果连接数量已满,客户端将收到WSAAECONNREFUSED的错误。
4.接受连接请求
SOCKET accept(
SOCKET s, //服务端的套接字
const struct sockaddr* name, //一个指向sockaddr_in的指针,用于获取对方的地址信息
int *addrlen //指向地址长度的指针
);
该函数在s上取出未处理连接中的第一个连接,然后为这个连接创建新的套接字,返回它的句柄,新创建的套接字是处理实际连接的套接字,它与s有相同的属性
默认是阻塞模式,会一直accept下去,直到有新的连接发生才返回。
addrlen用于指定addr所指空间的大小,也返回实际地址的长度,如过addr或addrlen是NULL,则没有关于远程地址的信息返回。
客户端在创建套接字成功后,要使用connect函数请求连接
int connect(
SOCKET s, //
const struct sockaddr FAR * name, //服务器的地址信息
int namelen //sockaddr_in 的长度
);
5.收发数据 默认是阻塞
send() 直到数据发送完毕或出错才返回
recv() 尽可能多的返回当前可用信息,直到达到缓冲区指定的大小
*************************************************************************************************
* 初始化Winsock *
* Server Client *
* socket() socket() //不需要绑定,系统会自动安排。 *
* bind() connect() *
* listen() *
* accept() recv()/send() *
* closesocket() *
* 释放Winsock *
*************************************************************************************************
UDP
UDP用的是sendto 和 recvfrom
int sendto(
SOCKET s,
const char FAR * buf, //发送数据缓冲区
int len, //发送的长度
int flags, //一般指定为0
const struct sockaddr FAR * to, //目的地址的sockaddr_in
int tolen //sockaddr_in的大小
);
int recvfrom(
SOCKET s,
char FAR * buf, //接收数据缓冲区
int len, //接收长度
int flags, //0
struct sockaddr FAR * from, //返回发送方的sockaddr_in
int FAR * fromlen //sockadddr_in的长度
);
注意:创建套接字之后,如果先调用sockto,则可以不用bind()显式的绑定本地地址,系统会自动绑定,所以后面调用recvfrom()也不会报错。
如果先调用recvfrom(),就会出错,因为没有绑定本机地址。
---------------------------------------------------------------------------------------------------
TCP UDP
Server socket()
bind()
listen()
recv()/send recvfrom()/sendto()
closesocket()
Client
socket()
recv()/send recvfrom()/sendto()
closesocket()
----------------------------------------------------------------------------------------------------
2.4 网络对时程序
时间协议[Time Protocol](RFC-868)是一种引用层协议。它返回一个为格式化的32位二进制数字,这个数字描述了从1900年1月1日0时0分0秒到现在的秒数,
以TCP/UDP的格式返回相应。
将服务器时间转换为本地时间是客户端的责任(借用文件时间)。
工作过程:
S : 监听37端口
C : 连接到37端口
S : 以32位二进制(unsigned int)数据发送时间。
C : 接收时间
C : 关闭连接
S : 关闭连接
如果服务器不能决定用什么时间,服务器会拒绝连接或不发生任何数据直接关闭连接。