您现在的位置是:首页 >技术教程 >Windows下的网络编程Winsock网站首页技术教程

Windows下的网络编程Winsock

森明帮大于黑虎帮 2024-09-06 00:01:03
简介Windows下的网络编程Winsock

前言

注意ws2_32.lib库路径:C:Program Files (x86)Windows Kits10Lib10.0.19041.0umx64。

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

相比于基础,网络编程就要复杂一些,但其实也有固定格式,记住即可。

首先是需要的头文件和库:

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

1、服务器下的Winsock

主要流程及主要函数:

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

WinsockWindows下的网络编程接口,它是由Unix下的BSDSocket发展而来,是一个与网络协议无关的编程接口。

1.1、构建编程环境:

Winsock在常见的Windows平台上有两个主要的版本,即Winsock1Winsock2。编写与Winsock1兼容的程序你需要引用头文件WINSOCK.H,如果编写使用Winsock2的程序,则需要引用WINSOCK2.H。此外还有一个MSWSOCK.H头文件,它是专门用来支持在Windows平台上高性能网络程序扩展功能的。使用WINSOCK.H头文件时,同时需要库文件WSOCK32.LIB,使用WINSOCK2.H时,则需要WS2_32.LIB,如果使用MSWSOCK.H中的扩展API,则需要MSWSOCK.LIB。正确引用了头文件,并链接了对应的库文件,你就构建起编写WINSOCK网络程序的环境了。

1.2、WSAData结构体

WSAData功能是:存放**windows socket**初始化信息。结构体如下:

struct WSAData {
    WORD wVersion;
    WORD wHighVersion;
    char szDescription[WSADESCRIPTION_LEN+1];
    char szSystemStatus[WSASYSSTATUS_LEN+1];
    unsigned short iMaxSockets;
    unsigned short iMaxUdpDg;
    char FAR * lpVendorInfo;
};
  • wVersion为你将使用的Winsock版本号。
  • wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。
  • szDescriptionszSystemStatus由特定版本的Winsock设置,实际上没有太大用处。
  • iMaxSockets表示最大数量的并发Sockets,其值依赖于可使用的硬件资源。
  • iMaxUdpDg表示数据报的最大长度;然而,获取数据报的最大长度,你需要使用WSAEnumProtocols对协议进行查询。最大数量的并发Sockets并不是什么神奇的数字,它是由可用的物理资源来决定的。
  • lpVendorInfo是为Winsock实现而保留的制造商信息,这个在Windows平台上并没有什么用处。

1.3、WSAStartup初始化Winsock

进行Winsock编程时,首先必须调用WSAStartup函数,设置程序中用到的Winsock版本,并初始化相应版本的库。该函数用于初始化网络环境,参数基本上是固定写法,记住即可,必须要有。

#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 成功时返回0,失败时返回非零的错误代码值。
// wVersionRequested   程序员要用的Winsock版本信息。
// lpWSAData           WSADATA结构体变量的地址值。

#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;
	}
}

有必要给出上述两个参数的详细说明。先说第一个,Winsock中存在多个版本,应准备WORD 类型的(WORD是通过typedef声明定义的unsigned short类型)套接字版本信息,并传递给该函数的第一个参数wVersionRequested

  • 若版本为1.2,则其中1是主版本号,2是副版本号,应传递0x0201。

  • 如前所述,高8位为副版本号,低8位为主版本号,以此进行传递。本书主要使用2.2版本,故应传递0x0202

  • 不过,以字节为单位手动构造版本信息有些麻烦,借助MAKEWORD宏函数则能轻松构建WORD型版本信息。

    • MAKEWORD(1,2) //主版本为1,副版本为2,返回0x0201。

    • MAKEWORD(2,2) //主版本为2,副版本为2,返回0x0202。

接下来讲解第二个参数lpWSADATA,此参数中需传入WSADATA型结构体变量地址LPWSADATAWSADATA的指针类型)。调用完函数后,相应参数中将填充已初始化的库信息。虽无特殊含义,但为了调用函数,必须传递WSADATA结构体变量地址。

下面给出WSAStartup函数调用过程,这段代码几乎已成为Winsock编程的公式

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

int main(int argc, char* argv[])
{
    WSADATA wsaDAta;
    // ...
    if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0) {
        ErrorHandling("WSAStartup() error!");
    }
    // ...
    return 0;
}

每个Winsock程序必须使用WSAStartup函数载入合适的Winsock动态链接库,如果载入失败,WSAStartup函数将返回SOCKET_ERROR,这个错误就是WSANOTINITIALISEDWSAStartup函数的定义如下:

int WSAStartup(
    WORD wVersionRequested,
    LPWSADATA lpWSAData
);
  • wVersionRequested指定了你想载入的Winsock版本,其高字节指定了次版本号,而低字节指定了主版本号。你可以使用宏MAKEWORD(x, y)来指定版本号,这里x代表主版本,而y代表次版本。
  • lpWSAData是一个指向WSAData结构的指针,WSAStartup函数会向该结构中填充其载入的Winsock动态链库的信息。
typedef struct WSAData{
    WORD           wVersion;
    WORD           wHighVersion;
    char           szDescription[WSADESCRIPTION_LEN + 1];
    char           szSystemStatus[WSASYS_STATUS_LEN + 1];
    unsigned short iMaxSockets;
    unsigned short iMaxUdpDg;
    char FAR *     lpVendorInfo;
} WSADATA, * LPWSADATA; 
  • wVersion为你将使用的Winsock版本号,wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。

  • szDescription与szSystemStatus由特定版本的Winsock设置,实际上没有太大用处。

  • iMaxSockets表示最大数量的并发Sockets,其值依赖于可使用的硬件资源。

  • iMaxUdpDg表示数据报的最大长度;然而,获取数据报的最大长度,你需要使用WSAEnumProtocols对协议进行查询。

  • 最大数量的并发Sockets并不是什么神奇的数字,它是由可用的物理资源来决定的。

  • lpVendorInfo是为Winsock实现而保留的制造商信息,这个在Windows平台上并没有什么用处。

Windows 95以后的操作系统都支持Winsock 2.2的版本.即使是这样,你也不能认为这些Windows平台支持最新的Winsock版本,为了让你的程序能够运行于大多数平台,最好使用Winsock1.1规范。

1.4、WSACleanup释放Winsock

当你使用完Winsock接口后,要调用下面的函数对其占用的资源进行释放:

#include<winsocket2.h>
int WSACleanup(void);

成功时返回 0,失败时返回 SOCKET_ERROR。调用该函数时,Winsock相关库将归还Windows操作系统,无法再调用Winsock相关函数。从原则上讲,无需再使用Winsock函数时才调用该函数,但通常都在程序结束之前调用。

如果调用该函数失败也没有什么问题,因为操作系统为自动将其释放,对应于每一个WSAStartup函数调用都应该有一个WSACleanup函数调用。

错误处理:Winsock函数调用失败大多会返回 SOCKET_ERROR(实际上就是-1),你可以调用WSAGetLastError函数得到错误的详细信息:

int WSAGetLastError (void);

对该函数的调用将返回一个错误码,其码值在WINSOCK.HWINSOCK2.H(根据其版本)中已经定义,这些预定义值都以WSAE开头.同时你还可以使用WSASetLastError来自定义错误码值。

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

void main(void){
   WSADATA wsaData;
    
    int Ret;

   // Initialize Winsock version 2.2

   if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
   {
      // NOTE: Since Winsock failed to load we cannot use
      // WSAGetLastError to determine the specific error for
      // why it failed. Instead we can rely on the return
      // status of WSAStartup.

      printf("WSAStartup failed with error %d
", Ret);
      return;
   }

   // Setup Winsock communication code here

   // When your application is finished call WSACleanup
   if (WSACleanup() == SOCKET_ERROR)
   {
      printf("WSACleanup failed with error %d
", WSAGetLastError());
   }
    	
}

1.5、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 serverSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建套接字
    if (serverSock == INVALID_SOCKET)
    {
        cout << "socket failed!" << endl;
        WSACleanup(); //释放Winsock库资源
        return 1;
    }

该代码创建了IPv4类型的地址,TCP协议的套接字:
在这里插入图片描述

网络编程就是编写程序使两台连网的计算机相互交换数据。这就是全部内容了吗?是的!网络编程要比想象中简单许多。那么,这两台计算机之间用什么传输数据呢?

  • 首先需要物理连接。如今大部分计算机都已连接到庞大的互联网,因此不用担心这点。

  • 在此基础上,只需考虑如何编写数据传输软件。但实际上这也不用愁,因为操作系统会提供名为**“套接字”(socket)**的部件。

  • 套接字是网络数据传输用的软件设备。

  • 即使对网络数据传输原理不太熟悉,我们也能通过套接字完成数据传输因此,网络编程又称为套接字编程。

  • 那为什么要用“套接字”这个词呢?

  • 我们把插头插到插座上就能从电网获得电力供给,同样,为了与远程计算机进行数据传输,需要连接到因特网。

  • 而编程中的“套接字”就是用来连接该网络的工具。

  • 它本身就带有“连接”的含义,如果将其引申,则还可以表示两台计算机之间的网络连接。

  • 套接字大致分为两种,其中,先要讨论的TCP套接字可以比喻成电话机。实际上,电话机也是通过固定电话网(telephone network)完成语音数据交换的。因此,我们熟悉的固定电话与套接字实际并无太大区别。

下面利用电话机讲解套接字的创建及使用方法:

  • 电话机可以同时用来connect拨打或listen接听,但对套接字而言,connect拨打和listen接听是有区别的。我们先讨论用于listen接听的套接字创建过程。

  • 接打电话需要电话机,有了电话机才能安装电话,接下来,我们就准备一部漂亮的电话机。

  • 下列函数创建的就是相当于电话机的套接字。

#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
// 成功时返回套接字句柄,失败时返回INVALID_SOCKET。

1.6、bind绑定套接字,调用其分配IP地址和端口号

函数原型:

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

使用:

#define _WINSOCK_DEPRECATED_NO_WARNINGS //vs环境下必须定义,否则无法使用inet_addr函数
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(12345);
serverAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //具体绑定本机的地址
if (bind(serverSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) //绑定套接字
{
    cout << "bind failed!" << endl;
    closesocket(serverSock); //关闭套接字
    WSACleanup(); //释放Winsock库资源
    return 1;
}

在这里插入图片描述

  • 我们只需购买机器,剩下的安装和分配电话号码等工作都由电信局的工作人员完成。
  • 而套接字需要我们自已安装,这也是套接字编程难点所在但多安装几次就会发现其实不难。
  • 准备好电话机后要考虑分配电话号码的问题,这样别人才能联系到自己。
  • 套接字同样如此。就像给电话机分配电话号码一样(虽然不是真的把电话号码给了电话机),利用以下函数给创建好的套接字分配地址信息(IP地址和端口号)
#include<winsock2.h>
int bind(SOCKET s, const struct sockaddr *name, int namelen);
// 成功时返回0,失败时返回SOCKETERROR。

1.7、listen监听套接字

函数原型:

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

使用:

if (listen(serverSock, SOMAXCONN) == SOCKET_ERROR) //监听套接字
{
    cout << "listen failed!" << endl;
    closesocket(serverSock); //关闭套接字
    WSACleanup(); //释放Winsock库资源
    return 1;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9bNK2U0-1686126105795)(C:UsersAdministratorAppDataRoamingTypora	ypora-user-imagesimage-20230607154514419.png)]

  • 调用bind函数给套接字分配地址后,就基本完成了接电话的所有准备工作。接下来需要连接电话线并等待来电。

  • 一连接电话线,电话机就转为可接听状态,这时其他人可以拨打电话请求连接到该机。同样,需要把套接字转化成可接收连接的状态。

#include <winsock2.h>
int listen(SOCKET s, int backlog);
// 成功时返回0,失败时返回SOCKETERROR。

1.8、accept接受客户端连接请求

函数原型:

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

使用:

SOCKET clientSock = accept(serverSock, NULL, NULL); //接受客户端连接
if (clientSock == INVALID_SOCKET)
{
    cout << "accept failed!" << endl;
    closesocket(serverSock); //关闭套接字
    WSACleanup(); //释放Winsock库资源
    return 1;
}

cout << "Client is connected." << endl;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQGqGQoZ-1686126105796)(C:UsersAdministratorAppDataRoamingTypora	ypora-user-imagesimage-20230607154655976.png)]

  • 连接好电话线后,如果有人拨打电话就会响铃,拿起话筒才能接听电话。

  • 拿起话筒意味着接收了对方的连接请求。套接字同样如此,如果有人为了完成数据传输而请求连接,就需要调用以下函数进行受理。

#include<winsock2.h>
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
// 成功时返回套接字句柄,失败时返回INVALID_SOCKET。

网络编程中接受连接请求的套接字创建过程可整理如下:

  • 第一步:调用socket函数创建套接字。
  • 第二步:调用bind函数分配IP地址和端口号。
  • 第三步:调用listen函数转为可接收请求状态。
  • 第四步:调用accept函数受理连接请求。

记住并掌握这些步骤就相当于为套接字编程勾勒好了轮廓,后续编程会为此轮廓着色。

1.9、closesocket关闭套接字

#include<winsock2.h>
int closesocket(SOCKET s);
// 成功时返回0,失败时返回SOCKETERROR。

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

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

1.10、windows下的io函数

1.10.1、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 << "发送信息失败";
}

Windows中则有些不同Windows严格区分文件I/O函数和套接字I/O函数。下面介绍Winsock数据传输 send函数。

#include<winsock2.h>
int send(SOCKET s, const char *buf, int len, int flags);
// 成功时返回传输字节数,失败时返回SOCKETERROR。
// s        表示数据传输对象连接的套接字句柄值。
// buf      保存待传输数据的缓冲地址值。
// len      要传输的字节数。
// flags    传输数据时用到的多种选项信息。

1.10.2、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;
}

介绍Winsock数据接收recv函数。

#include<winsock2.h>
int recv(SOCKET s, const char *buf, int len, int flags);
// 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
// s        表示数据接收对象连接的套接字句柄值。
// buf      保存接收数据的缓冲地址值。
// len      能够接收的最大字节数。
// flags    接收数据时用到的多种选项信息。

1.11、服务器完整代码

#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;
	}
	WSACleanup();
}

2、客户端下的Winsock

主要流程和函数:

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

其它三个函数与服务器一样,只是多出个connect函数,使用方法也与bind函数类似

2.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.2、客户端完整代码

#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();
}

3、windows下服务器代码展示

#include <WinSock2.h>
#include <cstring>
#include <iostream>
#include <string>

int main(int argc, char* argv[])
{
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        return 0;
    }

    // 初始化库
    WSADATA wsaData;
    int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (stu != 0) {
        std::cout << "WSAStartup 错误:" << stu << std::endl;
        return 0;
    }

    // 创建socket
    SOCKET servSock = socket(PF_INET, SOCK_STREAM, 0);
    if (servSock == INVALID_SOCKET) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }

    // 初始化服务器地址信息
    sockaddr_in servAddr;
    std::memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(std::atoi(argv[1]));

    // 调用bind函数绑定地址信息
    stu = bind(servSock, (sockaddr*)&servAddr, sizeof(servAddr));
    if (stu == SOCKET_ERROR) {
        closesocket(servSock);
        std::cout << "bind 错误" << std::endl;
        return 0;
    }

    // 调用listen函数,进入监听状态
    stu = listen(servSock, 5);
    if (stu == SOCKET_ERROR) {
        closesocket(servSock);
        std::cout << "listen 错误" << std::endl;
        return 0;
    }

    sockaddr_in clntAddr;
    int clntAddrSize = sizeof(clntAddr);
    // 接收请求
    SOCKET clntSock = accept(servSock, (sockaddr*)&clntAddr, &clntAddrSize);
    if (clntSock == INVALID_SOCKET) {
        closesocket(servSock);
        std::cout << "accept 错误" << std::endl;
        return 0;
    }

    std::string msg = "Hello World!";
    send(clntSock, msg.c_str(), msg.size(), 0);

    closesocket(clntSock);
    closesocket(servSock);

    // 清理库
    WSACleanup();

    return 0;
}

3、windows客户端代码展示

#include <WinSock2.h>
#include <cstring>
#include <iostream>
#include <string>

int main(int argc, char* argv[])
{
    if (argc != 3) {
        std::cout << "Usage: " << argv[0] << " IP port" << std::endl;
        return 0;
    }

    // 初始化库
    WSADATA wsaData;
    int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (stu != 0) {
        std::cout << "WSAStartup 错误:" << stu << std::endl;
        return 0;
    }

    // 创建socket
    SOCKET clntSock = socket(PF_INET, SOCK_STREAM, 0);
    if (clntSock == INVALID_SOCKET) {
        std::cout << "socket 错误" << std::endl;
        return 0;
    }

    // 初始化服务端地址信息
    sockaddr_in servAddr;
    std::memset(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    servAddr.sin_port = htons(std::atoi(argv[2]));

    // 连接到服务端
    stu = connect(clntSock, (sockaddr*)&servAddr, sizeof(servAddr));
    if (stu == SOCKET_ERROR) {
        closesocket(clntSock);
        std::cout << "connect 错误" << std::endl;
        return 0;
    }

    char msg[30] = { 0 };
    int strLen = recv(clntSock, msg, sizeof(msg) - 1, 0);
    if (strLen == -1) {
        closesocket(clntSock);
        std::cout << "recv 错误" << std::endl;
        return 0;
    }

    std::cout << "接收:" << msg << std::endl;
    closesocket(clntSock);

    WSACleanup();

    return 0;
}

4、解释socket

4.1 、正文

要想解释清楚Socket,首先要知道TCP,要想知道TCP,那就得对TCP/IP的体系结构以及每一层的大概工作有所了解,那么我们就先来说说TCP/IP的分层。

4.2、TCP/IP体系结构

首先简单说一下OSI参考模型,OSI将网络分为七层,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层、应用层,而TCP/IP体系结构则将网络分为四层,自下而上分别是网络接口层、网络层、传输层、应用层。为了将每一层讲明白,我们讲网络接口层拆分为物理层和数据链路层,我在《图解TCP/IP》上面找了一张OSI参考模型和TCP/IP体系结构的对照图,大家可以看一下:
在这里插入图片描述

介绍了了TCP/IP有哪几层后,再来说一说每一层的大概功能。计算机的世界很奇妙,它里面有很多东西和现实世界都是一一对应的,这也可能是计算机设计者们有意而为之的吧。我先来说一下一个数据包在网络中的传输过程,再来用物流的例子对照着解释一遍,你就应该能够明白每一层的作用了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-guLSkVpC-1686126105797)(C:UsersAdministratorAppDataRoamingTypora	ypora-user-imagesimage-20230607151543291.png)]

看上面的图,发送端想要发送数据到接收端。首先应用层准备好要发送的数据,然后给了传输层传输层的主要作用就是为发送端和接收端提供可靠的连接服务传输层将数据处理完后就给了网络层

网络层的功能就是管理网络,其中一个核心的功能就是路径的选择(路由),从发送端到接收端有很多条路,网络层就负责管理下一步数据应该到哪个路由器。选择好了路径之后,数据就来到了数据链路层,这一层就是负责将数据从一个路由器送到另一个路由器。然后就是物理层了,可以简单的理解,物理层就是网线一类的最基础的设备。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exWb9f5m-1686126105798)(C:UsersAdministratorAppDataRoamingTypora	ypora-user-imagesimage-20230607151651465.png)]

小明住在上海市长江路幸福小区5#666,现在小明在京东上面买了一部小米10Pro。京东在接到小米的订单后,工作人员从仓库中找到一部小米10Pro(应用层)。工作人员将手机打包好, 交给了京东物流(传输层)。接下来手机就到了转运中心(路由器),转运中心根据时间,成本等一系列因素决定下一步该发往哪一个转运中心(网络层)。决定好接下来发往哪一个转运中心后就开始用货车运输了,那么运输的过程就是数据链路层了,数据链路层负责将数据从一个端点送到另一个端点。那么货车行驶的道路就是物理层。几经周转,手机安全地送到了小明手上。

看完这个例子后,我相信大家就应该明白了数据在网络中转发的过程以及每一层的作用了吧。那么接下来我再来介绍一下TCP

4.3、TCP协议

先来看一下百度百科对于TCP协议的定义:**传输控制协议(TCP,Transmission Control Protocol)**是一种面向连接的、可靠的、基于字节流的传输层通信协议。这段文字是什么意思呢,我们再接着说上面一个例子。

发货之前工作人员首先得要确认一下路是不是通吧,比如现在是过年,物流全部停运不就是路不通吗,那还发什么货呀。要是没什么问题,物流全部正常运作就发货呗。手机到达小明家后,小明先拆开看看手机在运输途中有没有损坏,有损坏就联系客服处理,没问题就确认收货。再回到上面的定义中,面向连接指的是先建立连接再发送数据,也就是先确认路可以走得通再发货。可靠就是如果货物在运输过程中有损坏或者丢失就让京东重新发货,确保小明收到的手机是没有任何问题的。基于字节流的意思就是比如小明买了手机又买了手机配件,分开发货,几件物品不是在一个包裹里,一个一个发。在这个例子中,京东的工作人员和小明充当了TCP协议的角色,他们两个共同确保了货物的完整性

4.4、Socket

解释了TCP/IP体系结构以及TCP协议的大概内容后就可以来说一说什么是Socket了。还是先来看一下百度百科对于Socket的介绍:套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合

我们将一个小区比作一台计算机,一台计算机里面跑了很多程序,怎么区分程序呢,用的是端口,就好像小区用门牌号区分每一户人家一样。手机送到小明家了,怎么进去呢?从大门进啊,怎么找到大门呢?门牌号呀。不就相当于从互联网来的数据找到接收端计算机后再根据端口判断应该给哪一个程序一样吗。小明家的入口就可以用小区地址+门牌号进行唯一表示,那么同样的道理,程序也可以用IP+端口号进行唯一标识。那么这个程序的入口就被称作Socket

现在再来说说什么是Socekt编程,我们将TCP协议简化一下,就只有三个核心功能:建立连接、发送数据以及接收数据
在这里插入图片描述

5、其它网络相关函数

5.1、htons,ntohs等

这种函数名有固定的意义:

  • h:home。 本机
  • n:network。网络
  • s:short。 short类型
  • l:long。 long类型

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

还有htonl,htonll,htonf等也是类似的意思。

5.2、inet_addr,inet_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字符串

5.3、gethostbyname

通过域名获取ip地址,比如我们常见的www.baidu.comip地址是多少呢?就可以通过这个函数获取

使用方法:

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

5.4、注意事项

这些函数都被微软定为不安全函数,想正常使用就必须在代码最前面定义宏:

#define _WINSOCK_DEPRECATED_NO_WARNINGS

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。