6.Windows平台C++网络编程详解

一、前言

相比于C/C++基础内容,想要用C/C++原生开发网络程序就是一点有点难度的事情了。

不过虽然有点难度,但其实也都是一些固定格式而已,记住即可。

比如widnows网络编程就需要基本的头文件和库:

#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")

下面会对服务器、客户端的网络编程逻辑分别进行介绍,但也仅限基础的内容。

如果你想要进阶学习网络编程,可以参考本站的另外几篇文章,让你更快掌握网络开发的精髓:

二、服务器

开发服务器主要的流程和其用到的函数如下:

  1. 网络环境初始化:WSAStartup
  2. 创建服务器套接字:socket
  3. 绑定本机IP和端口:bind
  4. 监听客户端:listen
  5. 等待客户端连接:accept
  6. 发送消息:send
  7. 接收消息:recv
  8. 关闭socket:closesocket
  9. 清除网络环境:WSACleanup

因为比较底层,所以步骤非常繁琐,一个一个来即可。

1.WSAStartup函数

首先就是这个用于初始化网络环境的函数,其函数原型为:

int WSAStartup(
WORD wVersionRequested, //版本号,使用MAKEWORD宏生成
LPWSADATA lpWSAData //数据
);
//返回值:0代表成功,否则失败

它的基本使用方法:

#include<WinSock2.h>
#include<iostream>
#pragma comment(lib,"ws2_32")
using namespace std;
int main() {
	WSADATA data;
	int ret=WSAStartup(MAKEWORD(2,2),&data);
	if (ret) {
		cout << "初始化网络错误!" << endl;
		return -1;
	}
}

它的参数大部分情况下都是固定写法,记住即可,必须要有。

其底层原理在于,它实际上是来加载Windows Socket动态库的。

第一个wVersionRequested参数就是用来指定准备加载动态库的版本号,高字节为库文件的副版本,低字节指定主版本,而MAKEWORD(X,Y)宏用于生成该参数,其中X为高字节,Y为低字节。

而第二个lpWSAData 为指向WSADATA结构体的指针,该参数用于返回被加载动态库的有关信息:

typedef struct WSAData {
WORD  			wVersion; //期望调用者使用的socket版本(或实际返回的socket版本,可根据此参数判断返回的版本号是否正确,可通过HIBYTE宏取得高字节,LOBYTE宏取得低字节)
WORD  			wHighVersion; //本机Dll支持的最高版本
unsigned short  iMaxSockets;//一个进程最多可以打开的套接字数量(2.0版本后忽略此值)
unsigned short  iMaxUdpDg; //一个进程发送或接收的最大数据报长度
char FAR *   	lpVendorInfo; //厂商专有信息(2.0版本后忽略此值)
char    		szDescription[WSADESCRIPTION_LEN+1]; //DLL的描述信息
char    		szSystemStatus[WSASYS_STATUS_LEN+1];//DLL的状态信息
}

2.socket函数

它的函数原型为:

SOCKET socket(
int af,	//地址类型,常用IPv4地址:AF_INET,和IPv6地址:AF_INET6
int type, //套接字类型,常用TCP协议:SOCK_STREAM,UDP协议:SOCK_DGRAM
int protocol //协议类型,一般填0,自动选择即可
);
//返回值:为INVALID_SOCKET则失败,该宏实则定义为-1,否则成功

使用方法:

SOCKET sock=socket(AF_INET,SOCK_STREAM,0);
if (sock == -1) {
	cout << "创建套接字失败";
	return -1;
}

该代码创建了IPv4类型的地址,TCP协议的套接字。

3.bind函数

函数原型:

int bind( 
SOCKET s, //创建的socket
sockaddr * name, //包含地址和端口的结构体
int namelen //sockaddr 结构长度
);
//返回值:返回SOCKET_ERROR失败,该宏被定义为-1,否则成功,返回值为0

使用方法:

#define _WINSOCK_DEPRECATED_NO_WARNINGS //vs环境下必须定义,否则无法使用inet_addr函数
sockaddr_in addr;
addr.sin_family = AF_INET; //地址为IPv4协议
addr.sin_port = htons(9999); //端口为9999
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //绑定到本机的ip地址
ret=bind(sock,(sockaddr*)&addr, sizeof(addr)); //绑定
if (ret == -1) {
	cout << "绑定地址端口失败";
	return -1;
}

4.listen函数

函数原型:

int listen(
SOCKET s, //要监听的socket
int backlog //等待连接的最大队列长度
);
//返回值:返回SOCKET_ERROR失败,该宏被定义为-1,否则成功,返回值为0

使用方法:

ret=listen(sock,5);
if (ret == -1) {
	cout << "监听套接字失败";
	return -1;
}

5.accept函数

函数原型:

SOCKET accept(
SOCKET s, //接收的socket
sockaddr* addr, //接收到客户端的地址信息
int * addrlen //地址信息长度
);
//返回值:返回INVALID_SOCKET失败,该宏定义为-1,否则成功返回客户端的套接字,可进行发送和接收消息

使用方法:

sockaddr addrCli;
int len = sizeof(addrCli);
SOCKET sockCli=accept(sock,&addrCli,&len);
if (sockCli == -1) {
	cout << "接收客户端连接失败";
	return -1;
}

6.send函数

函数原型:

int send(
SOCKET s,
char * buf,//要发送的内容
int len, //内容长度
int flags //一般为0,拷贝到程序中就立即删除内核中的数据,或MSG_DONTROUTE:要求传输层不要将数据路由出去,MSG_OOB:标志数据应该被带外发送
);
//返回值:-1(或宏SOCKET_ERROR)表示发送失败,否则返回发送成功的字节数

使用方法:

char buf[0xFF] = "我是服务器";
ret=send(sockCli, buf, strlen(buf),0);
if (ret == -1) {
	cout << "发送信息失败";
}

7.recv函数

函数原型:

int recv(
SOCKET s, //套接字
 char * buf, //接受数据的缓存区
int len, //缓存区大小
int flags //标志,一般填0,将消息拷贝到应用程序中,将内核中的数据删除,还可以填MSG_PEEK,只取数据,不从内核中删除数据,MSG_OOB:处理带外数据
);
//返回值:小于等于0都表示出错,大于0则表示接收成功的数据大小

使用方法:

ret=recv(sockCli,buf,0xFF,0);
if (ret <= 0) {
	cout << "接受客户端数据失败";
	return -1;
}

注意这里的0xFF,是十六进制数字。

8.closesocket函数

int closesocket(
SOCKET s //要关闭的socket
);

该函数就是关闭不用的socket,用来释放资源。

9.WSACleanup函数

无任何参数,直接调用即可

WSACleanup();

按理说尽量在应用程序退出时都要进行清理,否则下次启动可能出现错误。

10.完整示例代码

最后给一份完整的示例代码,也就是一个最简单的windows服务器,它可以简单执行一次发送数据和一次接受数据:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<WinSock2.h>
#pragma comment(lib,"ws2_32")
#include<iostream>
using namespace std;
int main() {
	WSADATA data;
	int ret = WSAStartup(MAKEWORD(2, 2), &data);
	if (ret) {
		cout << "初始化网络错误!" << endl;
		WSACleanup();
		return -1;
	}
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == -1) {
		cout << "创建套接字失败";
		WSACleanup();
		return -1;
	}
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(9999);
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	ret = bind(sock, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		cout << "绑定地址端口失败";
		WSACleanup();
		return -1;
	}
	ret = listen(sock, 5);
	if (ret == -1) {
		cout << "监听套接字失败";
		WSACleanup();
		return -1;
	}
	sockaddr addrCli;
	int len = sizeof(addrCli);
	SOCKET sockCli = accept(sock, &addrCli, &len);
	if (sockCli == -1) {
		cout << "接收客户端连接失败";
		WSACleanup();
		return -1;
	}
	char buf[0xFF] = "我是服务器";
	ret = send(sockCli, buf, strlen(buf), 0);
	if (ret == -1) {
		cout << "发送信息失败";
		WSACleanup();
		return -1;
	}
	ret = recv(sockCli, buf, 0xFF, 0);
	if (ret <= 0) {
		cout << "接受客户端数据失败";
		WSACleanup();
		return -1;
	}
	printf("%s",buf);
	WSACleanup();
}

三、客户端

客户端的流程比服务器要简单一些,且很多函数也都是通用的:

  1. 初始化网络环境:WSAStartup
  2. 创建套接字:socket
  3. 连接服务器:connect
  4. 发送数据:send
  5. 接收数据:recv
  6. 清理网络环境:WSACleanup

这里只是多出了个connect函数,但其使用方法其实与bind函数也是类似的。

1.connect函数

函数原型:

int connect(
SOCKET s, //与服务器连接的socket
sockaddr* name, //服务器的地址端口
int namelen //上个参数结构体的长度
);
//返回值:-1失败,否则成功

使用方法:

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9999);
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
int ret = connect(sock, (sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
	cout << "连接服务器失败" << endl;
	return -1;
}

2.完整示例代码

同样的,这里也给一份widnwos客户端的完整示例代码,可以与前面的服务器完成一次接收、发送数据:

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<WinSock2.h>
#include<iostream>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
	WSADATA data;
	int ret = WSAStartup(MAKEWORD(2, 2), &data);
	if (ret) {
		cout << "初始化网络错误!" << endl;
		WSACleanup();
		return -1;
	}
	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(9999);
	addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	int ret = connect(sock, (sockaddr*)&addr, sizeof(addr));
	if (ret == -1) {
		WSACleanup();
		cout << "连接服务器失败" << endl;
		return -1;
	}
	char buf[0xFF]{};
	ret=recv(sock,buf,sizeof(buf),0);
	if (ret <= 0) {
		WSACleanup();
		cout << "接收服务器数据失败" << endl;
		return -1;
	}
	cout << "服务器:" << buf << endl;

	ret=send(sock,buf,ret,0); //将接收到的数据发回服务器
	if (ret <= 0) {
		WSACleanup();
		cout << "发送服务器数据失败" << endl;
		return -1;
	}
	WSACleanup();
}

四、其它网络相关函数

首先是网络字节序与本机字节序互相转换的函数,比如htons,ntohs等,它们的名字其实都有固定的含义:

  • hhome
  • nnetwork
  • sshort
  • llong

htons:意思就是本机字节序转到网络字节序short类型的长度。 ntohs:意思就是网络字节序转到本机字节序short类型的长度。

除此之外还有htonl,htonll,htonf等也是类似的意思,f就是floatlllong long

其一般用于端口的转化。

还有ip地址的转化,有inet_addrinet_ntoa这两个函数:

  • inet_addr:负责将我们平时看到的网络地址127.0.0.1等转化为网络字节序。
  • inet_ntoa:负责将网络字节序还原为我们平时看到的字符串127.0.0.1等。

使用方法:

sockaddr_in addr;
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //将127.0.0.1转换为网络字节序
char* c_IP = inet_ntoa(addr.sin_addr);//将网络字节序转换为127.0.0.1字符串

除此之外,还有可以通过域名获取ip地址的函数:gethostbyname

比如本站域名www.kucoding.com的ip地址是多少呢?就可以通过这个函数获取!

使用方法:

//获取主机ip
HOSTENT* host = gethostbyname("www.kucoding.com"); //如获取网站IP地址,参数填写域名即可,不需加"http://"
if (host == NULL)
{
	return false;
}
//转化为char*并拷贝返回
cout << inet_ntoa(*(in_addr*)*host->h_addr_list);

五、注意事项

由于很多函数都被vs认定为了不安全的函数,所以想要正常使用,一般还得在代码前面定义一个宏:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

当然,你也可以使用vs提供的相应的安全函数来替代这个不安全函数,不过普遍来说,相应的安全函数使用起来都会更加的繁琐。