必须使用到的头文件是winsock2.h
和winsock.h
两个头文件中选择一个。
需要注意的是,单纯的引入这个头文件是不能完成代码的编译的。因为winsock
需要使用静态链接库WSOCK32.LIB进行静态链接。因此需要采用以下其中之一的方式将静态库进入,否则在编译的时候会出现以下的错误:
NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSAStartup,该符号在函数 main 中被引用
NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSACleanup,该符号在函数 main 中被引用
NetDemo-12ac36.o : error LNK2019: 无法解析的外部符号 __imp_WSAGetLastError,该符号在函数 main 中被引用
#pragma comment(lib, "WSOCK32")
clang++ .\NetDemo.cpp -o NetDemo.exe -L F:\uCard\VC6.0green\VC98\Lib\ -lWSOCK32
包含头文件之后,需要的做的第二件事就是将winsock
进行初始化,初始化的过程主要是使用这个函数加载合适的winsock DLL
。然后才能进行使用,使用的初始化函数原型如下:
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
然后使用winsock
进行socket编程,使用完winsock
之后,最终需要将socket进行释放资源,使用的是以下的函数原型如下:
int WSACleanup();
本函数原型的使用实例是:
WSAData wsadata;
WSAStartup(MAKEWORD(2, 2), &wsadata);
WSACleanup();
在进行编程的时候需要对在网络中出现的问题进行错误检查和错误处理,在这里使用的函数原型如下所示:
int WSAGetLastError();
这个函数接收的错误必须是在winsock
被加载完成之后,才能进行错误的获取,否则不能获取错误。正是因为这个原因,这个函数调用需要在WSAStartup之后获取错误代码。
理解结构体——SOCKADDR_IN,这个结构体主要的是以下结构:
struct sockaddr_in
{
short sin_family; // 协议栈
u_short sin_port; // 端口号
struct in_addr sin_addr; // 自己的IP地址
char sin_zero[8];// 用于填充的字节
};
接下来分别介绍以上结构体中的各字段:
主要的问题就是将点分十进制转换成为一个长整型函数,主要的函数原型就是以下的函数:
unsigned long inet_addr(const char FAR * cp);
这样可能存在一个问题,对于不同的主机厂商,他们设定的IP地址在本机中的序列可能有所不同,即我们所说的大端字节序和小端字节序的区别。然而在同一个网络上就需要将以上的IP地址进行统一,在这里我们统一使用大端字节序作为网络上IP的标准。
于是针对这一需求,就存在大量的函数用于解决这个问题,将主机字节序转换成为网络字节序。下列的四个API都能实现以上的需求:
u_long htonl(u_long hostlong);// 返回值就是最终的数据 int WSAHtonl(SOCKET s, u_long hostlong, u_long FAR * lpnetlong);// 返回值放置在lpnetlong中 u_short htons(u_short hostshort); int WSAHtons(SOCKET s, u_short hostshort, u_short FAR * lpnetshort);
实现反方向转化的函数为:
u_long htonl(u_long netlong);// 返回值就是最终的数据 int WSAHtonl(SOCKET s, u_long netlong, u_long FAR * lphostlong);// 返回值放置在lpnetlong中 u_short htons(u_short netshort); int WSAHtons(SOCKET s, u_short netshort, u_short FAR * lphostshort);
敬请期待
socket在网络编程中是一个很重要的概念,针对socket这个结构体有以下的结构:
SOCKET socket(
int af; // 协议的地址族
int type; // 套接字的类型
int protocol; // 使用的协议
);
其实我们不难知道,socket是传输层的一个概念,因此以上的结构体如果我们采用TCP/IP协议族的话,那么以上结构体中各字段的含义如下:
这个过程让我们来梳理一下,我们所说的服务器本质上就是一个进程,如果想要被客户端连接,那么服务器必须在一个已知名称(其实就是一个socket上,我们知道socket就是IP地址+端口号,时刻注意这个需要绑定的地址服务就是服务器自己的IP)上进行监听。那么这样一来整个服务器需要做的工作就比较明了了——
int bind(
SOCKET s, // 等待客户连接的套接字,是客户的套接字
const struct sockaddr FAR * name, // 就是一个普通的缓冲区。根据使用的协议必须实际地填充一个地址缓冲区,并在调用时,将其转成一个sockaddr
int namelen// 由协议决定的要传递的地址结构的长度
);
SOCKET s;
SOCKETADDR_IN tcpaddr;
int port = 5150;
s = socket(AF_INET, sOCK_STREAM, IPPROTO_TCP);
tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));
int listen(
SOCKET s, // 这个是被绑定的套接字
int backlog // 被阻塞的连接的最大队列长度
);
SOCKET accept(
SOCKET s,
struct sockaddr FAR * addr, // 用来存储请求客户端的IP地址
int FAR * addrlen //
);
客户端创建 s o c k e t socket socket连接服务器的主要过程是以下几步:
创建一个 s o c k e t socket socket
建立一个 S O C K A D D R SOCKADDR SOCKADDR的地址结构,这个地址是准备连接到服务器的IP地址,在 T C P / I P TCP/IP TCP/IP协议中,这个地址结构就是监听服务器的IP地址和端口号
使用 c o n n e c t connect connect初始化客户机与服务器的连接
int connect(
SOCKET s,
const struct sockaddr FAR * name,
int namelen
);
对于 U D P UDP UDP连接的 s e r v e r server server的主要工作的流程是以下的几步:
1)初始化 s o c k e t socket socket
2)将 s o c k e t socket socket绑定自己的 i p ip ip地址和端口
3) r e c v f r o m recvfrom recvfrom以获取客户端的 i p ip ip地址,然后进行通信
4) s e n d t o sendto sendto发送数据
对于 U D P UDP UDP连接的 c l i e n t client client的主要步骤是以下几步:
1)初始化 s o c k e t socket socket
2)使用 s e n d t o sendto sendto向服务器的 s o c k e t socket socket发送数据
存在的疑问就是:QQ用户即使在对方离线的情况下,是怎样能够收取对方在自己离线时发送给自己的信息
实际上这个INADDR_ANY转换成为点分十进制是0.0.0.0
,这个地址让人感觉就比较迷茫。在网络地址中设置0.0.0.0
这个地址的用意实际上是这样的——
我们知道一台主机可能有多个IP地址,就比如我们之前学到的在进行环回测试时候的保留地址127.0.0.1
,这个地址是不可能出现在任何公网情况下的,当我们的主机连接网络时,就会获取路由器为我们分配的一个IP地址,这样一看,我们的就存在两个IP。还有其他更多的情况是可能服务器有多个网卡什么的可能会有更多的地址,因此一台主机的IP地址可能存在多个。
即然存在以上的问题,(需求)那么我们当然希望访问主机的任意一个IP地址都能够得到服务器的响应,这其实就是存在的问题。如果我们按照原来的思路,将服务器的socket只绑定在一个IP上,那么结果就是——客户端只能通过这个唯一的IP访问服务器,剩下的IP就不能进行访问了。按照原来的绑定方式,那么只能将每个IP地址都绑定一个socket进行管理,这样就会相当繁琐,因此有以的方法——
为了解决这个问题,于是设置将服务器的socket与0.0.0.0
进行绑定,这个特殊的IP地址表示的是本机的所有IP地址,这样理解的话,也就是说我们使用一个socket就监听了好多地址,而且最后只需要管理一个socket。这就大大简化了最后的管理过程。
在已经建立连接的套接字上发送数据的主要API是 s e n d send send,这个函数的原型是以下的方式:
int send(
SOCKET s,
const char FAR * buf,//指向缓冲区的指针
int len,//缓冲区的大小
int flags
);
主要的套接字函数接收是主要是API是 r e c v recv recv,这个函数的原型是以下的方式:
int recv(
SOCKET s,
char FAR * buf,
int len,
int flags
);
在发送的过程中,使用的接口是 s e n d t o sendto sendto,这个函数的原型是以下的方式:
int sendto(
SOCKET s,
const char FAR * buf,
int len,
int flags,
const struct sockaddr * FAR * to,
int len
);
在接收的过程当中,使用的接口是 r e c v f r o m recvfrom recvfrom,这个函数主要是以下的方式,通过这个函数可以获取发送方的IP地址和端口号.
int recvfrom(
SOCKET s,
char FAR * buf,
int len,
int flags,
struct sockaddr FAR * from,
int FAR * fromlen
);
在 w i n d o w s s o c k e t windows\quad socket windowssocket编程当中,也不是所有的都是一帆风顺的,因此对于错误的处理也应当足够引起我们的重视,一般来说,我们可以通过函数来获取 w i n s o c k winsock winsock的错误类型,从而定位错误。
int WSAGetLastError(void);
在连接过程当中发生错误之后,可以调用这个函数,就可以获取返回的错误的整数代码。这些值可能存在于 W I N S O C K 1. H WINSOCK1.H WINSOCK1.H或 W I N S O C K 2. H WINSOCK2.H WINSOCK2.H中。