WinSocket自学开始
一、声明
本人在自学过程中在此记录学习心得,如果内容有欠妥的地方还请在此下方批评,或私信,谢谢阅读。
二、常用的基本函数
2.1 头文件
使用WinSocket编写程序的时候,要用到几个重要的文件,winsock2.h
,静态链接库文件ws2_32.lib
,以及动态链接库ws2_32.dll
,头文件是用在源程序中,静态链接库是用来编译基于Winsock程序的,而动态链接库ws2_32.dll
是运行基于WinSock的程序所必须的。
2.2 函数
参考书目:《网络安全编程技术与实例》08年。
参考文档:微软文档
2.2.1 htonl函数
将主机的u_long
值转换成网络字节顺序(32位)。
//原型 u_long htonl(u_long hostlong)
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void main(){
u_long a = 0x12345678;
u_long b = htonl(a);
printf("%x\n", b);
}
2.2.2ntohl函数
将32位网络字节顺序转化为主机字节。
//原型 u_long ntohl(u_long netlong)
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void main(){
u_long a = 0x12345678;
u_long b = ntohl(a);
printf("%x\n", b);
}
2.2.3 ntohs函数
将16位网络字节转换为主机字节。
//原型 u_short ntohs(u_short netshort)
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void main(){
u_short a = 0x12345678;
u_short b = ntohs(a);
printf("%x\n", b);
}
2.2.4 htons函数
将16位主机字节转换为网络字节顺序。
//原型 u_short htons(u_short hostshort)
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
void main(){
u_short a = 0x12345678;
u_short b = htons(a);
printf("%x\n", b);
}
2.2.5 inet_pton 和 inet_ntop 函数
参考这篇(点击阅读)文章,其中的有关这两个函数都有解释,我们这里直接给出样例。
#include<WinSock2.h>
#include<stdio.h>
#pragma comment(lib, "ws2_32.lib")
#include <Ws2tcpip.h>
void main(){
char a[] = "192.168.1.3"; //创建一个字符串形式的IP地址
struct in_addr s; //创建一个接受IP地址转换后的变量,用来存储二进制形式的
if (inet_pton(AF_INET, a, &s) == 1)printf("ok\n"); //如果成功会返回1
printf("%s->%u\n", a, s);
if (inet_ntop(AF_INET, &s, a, sizeof(a)) != NULL)puts("ok"); //再转换回去看看
printf("%s->%u\n", a, s);
}
其中结构体in_addr
为:
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;
};
目前我们只需注意u_long S_addr
就可以了。
2.2.6 WSAStartup 和 WSACleanup函数
WSAStartup
其功能是为初始化Winsock,在使用套接字函数前都应该调用。其本质是调用合适的WinSock动态链接库。
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
第一个参数是WinSock版本,用MAKEWORD宏来表示,例如:MAKEWORD(2,1)表示2.1版本。
第二个参数表示WSAData指针,它用来存储套接字信息。
typedef struct WSAData{...}WSADATA,*LPWSADATA;
其内容需要的话百度关键字。
int WSACleanup(void);
此函数表示停止使用Winsock 2 DLL。
2.2.7 gethostname函数
次函数用来获取主机名称,直接看例子。
#include<WinSock2.h>
#include<stdio.h>
#pragma comment(lib, "ws2_32.lib")
#include <Ws2tcpip.h>
void main()[
char hostname[100]; //存放主机字符串的数组
WSADATA wsaData;
int Ret;
if (Ret = WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("uperror %d\n", Ret);
exit(EXIT_SUCCESS);
}
//函数的使用
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
printf("erroe gethostname");
exit(EXIT_SUCCESS);
}else {
printf("hostname:%s\n", hostname);
}
if (WSACleanup() == SOCKET_ERROR) {
printf("cleanerror\n");
exit(0);
}
}
2.2.8 getaddrinfo 和 getnameinfo函数
推荐两个中文的解释getaddrinfo和addrinfo。(其实是同一个)
另外我们也可以看看原官方文档。
以下是我的见解:
(首先我先从以上内容中摘取一些方便观看,如有争议可以私信)
getaddrinfo:
getaddrinfo
定义:
int getaddrinfo(const char *restrict nodename, /* host 或者IP地址 */
const char *restrict servname, /* 十进制端口号 或者常用服务名称如"ftp"、"http"等 */
const struct addrinfo *restrict hints, /* 获取信息要求设置 */
struct addrinfo **restrict res); /* 获取信息结果 */
void freeaddrinfo(struct addrinfo *ai);
前两个参数就不过多赘述了,我们看后两个参数,两个都是相同的结构体指针,hints是用来设置接收这个结构体的信息的。res用来接收最终信息的结构体指针的指针,所以我们要传进去一个指针的地址。
返回值: 成功返回0,否则返回一个非零,用gai_strerrorA(int ecode)
接受一个返回的字符串指针来检查错误。freeaddrinfo
用来释放获取的addrinfo
结构体或者结构体链表一系列的内存空间。
相关结构体:addrinfo
定义:
来自
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
在初始化这个结构体的时候我们一般都初始化为0就可以了。(目前来说)
这里我们再集中学习几个结构体一起来分析:sockaddr
定义:
struct sockaddr
{
sa_family_t sa_family; //ushort类型
char sa_data[14]; // Socket address (variable-length data).
};
sockaddr_in
定义:
struct sockaddr_in
{
sa_family_t sin_family; //AF_INET.
in_port_t sin_port; //Port number(网络字节序).
struct in_addr sin_addr; //IP address(网络字节序).
char[8] sin_zero; //目前我还不知道用途
};
sockaddr_in6
定义:
struct sockaddr_in6{
u_short sin6_family;
u_short sin6_port;
u_long sin6_flowinfo;
in6_addr sin6_addr;
u_long sin6_scope_id;
}
typedef struct sockaddr_in6 SOCKADDR_IN6;
typedef struct sockaddr_in6 *PSOCKADDR_IN6;
typedef struct sockaddr_in6 FAR *LPSOCKADDR_IN6;
sockaddr_in
和sockaddr_in6
分别对应IPv4和IPv6。
以上结构体关系我简单画了一个联系仅供参考:
其次sockaddr可以在使用的过程中强制转换成IPv4和IPv6的结构体类型,其中我们会发现IPv6结构体类型的内存大小会与sockaddr内存大小不一致,但在强制转换的过程中信息却可以完好的保留,在我的好友帮助下我们猜测应该是在使用内置函数的里面,存在一个内存分配的问题(因为结构体里面都是指针),所以强制转换可以实现应对不同的IP。
getnameinfo:
getnameinfo
定义:
INT WSAAPI getnameinfo(
const SOCKADDR *pSockaddr, //包含地址和端口号的sockaddr结构体
socklen_t SockaddrLength, //传入一个地址结构体的字节长度
PCHAR pNodeBuffer, //用来接收返回的主机名字符串
DWORD NodeBufferSize, //用于分配接收主机名字符串的空间,一般使用sizeof
PCHAR pServiceBuffer, //同理是接收服务名的
DWORD ServiceBufferSize, //同理一般使用sizeof
INT Flags
);
参考文档:链接
getnameinfo函数可以实现地址到主机名,端口号到服务名的解析。
参数说明:
-
pSockaddr
如果不是sockaddr类型,相应的IPv4传递sockaddr_in,IPv6传递sockaddr_in6。 -
SockaddrLength
如果是addrinfo指针,我们可以使用这个结构体里的ai_addrlen。如果是IPv4或者IPv6我们分别使用sizeof运算符来计算地址结构体的大小。 -
pNodeBuffer
和pServiceBuffer
是要预先分配好内存空间。 -
Flags
有以下几种设置:NI_NAMEREQD
被设置的时候,如果主机名不能被域名系统解析(DNS),则返回error。NI_NOFQDN
被设置的时候,会返回给host参数一个本地主机具有的相对辨别名。(我也不清楚,还是看原文档的英文吧o(╥﹏╥)o)NI_NUMERICHOST
被设置的时候,会返回一个主机名称的数字形式来代替他本身的名称。如果它的名称不能被域名系统解析(DNS),也会返回。NI_NUMERICSERV
被设置的时候,返回服务的端口号,同时,如果主机名没有设置一个IP地址的话,主机名会作为IP地址被返回。NI_DGRAM
Setting the NI_DGRAM flag indicates that the service is a datagram service. This flag is necessary for the few services that provide different port numbers for UDP and TCP service.
返回值: 成功返回零,非零使用WSAGetLastError(void)检查错误(整形返回值)。错误列表可以自行搜索。
family:
AF_INET
对应的IPv4;AF_INET6
对应IPv6;AF_UNSPEC
IPv4和IPv6都可以。
余下有关参数请参考文档
例子:
结合前面获取的主机名,我们来解析IP。
//解析地址
char hostname[100];
WSADATA wsaData;
int Ret;
if (Ret = WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("uperror");
exit(EXIT_SUCCESS);
}
if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) {
printf("erroe gethostname");
exit(EXIT_SUCCESS);
}else {
printf("hostname:%s\n", hostname);
}
//设置接收信息,创建接收返回信息的结构体指针getaddr,会在函数内分配空间
addrinfo *addrstr, *getaddr;
addrstr = (addrinfo*)malloc(sizeof(addrinfo));
//初始化为0
memset(addrstr, 0, sizeof(addrinfo));
//地址族类型设置为AF_UNSPEC,即可为IPv4也可为IPv6
addrstr->ai_family = AF_UNSPEC;
//创建临时变量
sockaddr_in* ipv4;
sockaddr_in6* ipv6;
int s;
if (s = getaddrinfo(hostname, "https", addrstr, &getaddr) != 0) {
printf("error getaddrstr:%s", gai_strerror(s));
exit(EXIT_SUCCESS);
}
//创建内存空间用来接收转换后的字符串
char buf[1025];
for (addrinfo* re = getaddr; re != NULL; re = re->ai_next) {
switch(re->ai_family) {
case AF_INET:
ipv4 = (sockaddr_in*)re->ai_addr;
inet_ntop(re->ai_family, &ipv4->sin_addr, buf, sizeof(buf));
break;
case AF_INET6:
ipv6 = (sockaddr_in6*)re->ai_addr;
inet_ntop(re->ai_family, &ipv6->sin6_addr, buf, sizeof(buf));
break;
}
printf("[ipv%d]:%s\n", re->ai_family == AF_INET ? 4: 6, buf);
}
puts("--------------------------------");
//反过来应用getnameinfo函数解析
int ret = 0;
addrinfo* res_p;
for (res_p = getaddr; res_p != NULL; res_p = res_p->ai_next) {
char host[1024], servername[1024] = {0};
memset(host, 0, sizeof(host));
puts("going");
ret = getnameinfo(res_p->ai_addr, res_p->ai_addrlen, host, sizeof(host), servername, sizeof(servername), NI_NAMEREQD);
if (ret != 0)
{
printf("getnameinfo: %s\n", gai_strerror(ret));
}
else
{
printf("hostname: %s %s\n", host, servername);
}
}
//释放刚刚创建的getaddr指针空间
freeaddrinfo(getaddr);
if (WSACleanup() == SOCKET_ERROR) {
printf("cleanerror\n");
exit(0);
}
2.2.9 WSAGetLastError函数
在WinSock编程中遇到错误可以用此函数检查。
int WSAGetLastError(void);