Windows网络编程(一)基础

Table of Contents

    • 准备工作
      • socket
      • C/S模式
    • 源代码
      • 服务端
      • 客户端
      • 源码分析
      • 数据传输
      • 关闭连接
    • 符号解释
      • WSAStartup
      • sin_family
      • sin_port
      • inet_addr
      • htonl / htons
      • af
      • type
    • C/S 通信

准备工作

Windows网络编程一般是指 Windows Socket 编程(winsocket),它从UNIX Socket 发展而来。进行Windows网络编程,首先需要添加依赖库WS2_32.libWSOCK_32.lib,加载动态库ws2_32.dll,放入C:/Windows/System32。然后使用时在源文件中包含头文件:

#include 
// #include 
// #include 

说明:
有些接口已经弃用,采用新的接口,具体是哪些,后面会慢慢指出。

  • 如何引入依赖库?
#pragma comment(lib, "ws2_32.lib");    // 源文件中添加

也可以在配置文件中添加:属性----链接器----输入----ws2_32.lib.

socket

socket 套接字是应用层到传输层的接口,表示一个连接的两端,每个端由IP地址和端口port组成,即socket是由两端点的ip和端口port组成的

  • 套接字类型 SOCKET 定义

    typedef unsigned int SOCKET;   // 句柄
    
  • 端口

    端口是传输层的概念,每个端口对应一个 process 进程,因此一条连接表示一个进程与另一个进程建立联系。

  • 套接字类型

    一般使用两种套接字:TCP 流套接字,UDP 数据报套接字。前者提供可靠的、无重复的、有序的数据流服务,后者提供不可靠传输。

C/S模式

winsocket 一般采用C/S模式

  • Server 端流程

1、初始化winsocket
2、建立socket
3、绑定服务端地址(bind)
4、开始监听(listen)
5、然后与客户端建立连接(accept)
6、然后与客户端进行通信(send, recv)
7、当通信完成以后,关闭连接
8、释放winsocket的有关资源

  • Client 端流程

1、初始化winsocket
2、建立socket
3、与服务器进行连接(connect)
4、与服务器进行通信(send, recv)
5、当通信完成以后,关闭连接
6、释放winsocket占用的资源

话不多说,先上一段代码,再小段分析

源代码

源码亲测可以运行

服务端

// win_server.cpp
// compiler with: VS2017
#include "pch.h"
#include   
#include   // 必须包含windwos.h之前
#include 
// #include   /* _beginthreadex */

// 指定依赖库目录
#pragma comment(lib,"ws2_32.lib") 
// 设置端口号
constexpr auto PORT = 6000;

// C/S 端连接情况分析
// Server 端收发数据情况
DWORD WINAPI clientProc(LPARAM lparam)
{
	SOCKET sockClient = (SOCKET)lparam;
	char buf[1024];
	while (TRUE)
	{
		memset(buf, 0, sizeof(buf));
		// 接收客户端的一条数据 
		int ret_recv = recv(sockClient, buf, sizeof(buf), 0);
		//检查是否接收失败
		if (SOCKET_ERROR == ret_recv)
		{
			printf("socket recv failed\n");
			closesocket(sockClient);
			return -1;
		}
		// 0 代表客户端主动断开连接
		if (ret_recv == 0)
		{
			printf("client close connection\n");
			closesocket(sockClient);
			return -1;
		}

		// 发送数据
		int ret_send = send(sockClient, buf, strlen(buf), 0);
		//检查是否发送失败
		if (SOCKET_ERROR == ret_send)
		{
			printf("socket send failed\n");
			closesocket(sockClient);
			return -1;
		}
	}
	closesocket(sockClient);
	return 0;
}

// 网络环境初始化
// 加载 dll,初始化socket
bool InitNetEnv()
{
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf("WSAStartup failed\n");
		return false;
	}
	return true;
}

int main(int argc, char * argv[])
{
	if (!InitNetEnv())
	{
		return -1;
	}
	// 初始化完成,创建一个TCP的socket
	SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	//检查是否创建失败
	if (sServer == INVALID_SOCKET)
	{
		printf("socket failed\n");
		return -1;
	}
	printf("Create socket OK\n");
	//进行绑定操作
	SOCKADDR_IN addrServ;
	addrServ.sin_family = AF_INET; // 协议簇为IPV4的
    // 端口  因为本机是小端模式,网络是大端模式,调用htons把本机字节序转为网络字节序
	addrServ.sin_port = htons(PORT); 
    // ip地址,INADDR_ANY表示绑定电脑上所有网卡IP
	addrServ.sin_addr.S_un.S_addr = INADDR_ANY; 
    
	//完成绑定操作
	int ret = bind(sServer, (sockaddr *)&addrServ, sizeof(sockaddr));
	//检查绑定是否成功
   
	if (SOCKET_ERROR == ret)
	{
		printf("socket bind failed\n");
		WSACleanup(); // 释放网络环境
		closesocket(sServer); // 关闭网络连接
		return -1;
	}
	printf("socket bind OK\n");
	// 绑定成功,进行监听
    
	ret = listen(sServer, 10);
	//检查是否监听成功
	if (SOCKET_ERROR == ret)
	{
		printf("socket listen failed\n");
		WSACleanup();
		closesocket(sServer);
		return -1;
	}
	printf("socket listen OK\n");
	// 监听成功
	sockaddr_in addrClient; // 用于保存客户端的网络节点的信息
	int addrClientLen = sizeof(sockaddr_in);
	while (TRUE)
	{
		//新建一个socket,用于客户端
		SOCKET *sClient = new SOCKET;
		//等待客户端的连接
		*sClient = accept(sServer, (sockaddr*)&addrClient, &addrClientLen);
		if (INVALID_SOCKET == *sClient)
		{
			printf("socket accept failed\n");
			WSACleanup();
			closesocket(sServer);
			delete sClient;
			return -1;
		}
		//创建线程为客户端做数据收发
		//_beginthreadex(NULL, 0, &clientProc, NULL, CREATE_SUSPENDED, (LPVOID)*sClient);
		CreateThread(0, 0, (LPTHREAD_START_ROUTINE)clientProc, (LPVOID)*sClient, 0, 0);
	}
	closesocket(sServer);
	WSACleanup();
	return 0;
}

客户端

// win_client.cpp
// compile with: VS2017
#include "pch.h"
#include 
#include 
#include 

#pragma warning(disable:4996)
#pragma comment(lib,"ws2_32.lib")
constexpr auto PORT = 6000;

int main(int argc, char * argv[])
{
	//初始化网络环境
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		printf("WSAStartup failed\n");
		return -1;
	}
	// 初始化完成,创建一个TCP的socket
	SOCKET sServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sServer == INVALID_SOCKET)
	{
		printf("socket failed\n");
		return -1;
	}
	//指定连接的服务端信息
	SOCKADDR_IN addrServ;
	addrServ.sin_family = AF_INET;
	addrServ.sin_port = htons(PORT);
	//客户端只需要连接指定的服务器地址,127.0.0.1是本机的回环地址
	addrServ.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	// 服务器 Bind 客户端是进行连接
	int ret = connect(sServer, (SOCKADDR*)&addrServ, sizeof(SOCKADDR));//开始连接
	if (SOCKET_ERROR == ret)
	{
		printf("socket connect failed\n");
		WSACleanup();
		closesocket(sServer);
		return -1;
	}
	//连接成功后,就可以进行通信了
	char szBuf[1024];
	memset(szBuf, 0, sizeof(szBuf));
	sprintf_s(szBuf, sizeof(szBuf), "Hello server");
	//当服务端是recv的时候,客户端就需要send,若两端同时进行收发则会卡在这里,因为recv和send是阻塞的
	ret = send(sServer, szBuf, strlen(szBuf), 0);
	if (SOCKET_ERROR == ret)
	{
		printf("socket send failed\n");
		closesocket(sServer);
		return -1;
	}

	ret = recv(sServer, szBuf, sizeof(szBuf), 0);
	if (SOCKET_ERROR == ret)
	{
		printf("socket recv failed\n");
		closesocket(sServer);
		return -1;
	}
	printf("%s\n", szBuf);

	closesocket(sServer);
	WSACleanup();
	return 0;
}

源码分析

  • WSAData 结构体
typedef struct WSAData {
        WORD                    wVersion;       // 版本号
        WORD                    wHighVersion;
#ifdef _WIN64
        unsigned short          iMaxSockets;
        unsigned short          iMaxUdpDg;
        char FAR *              lpVendorInfo;
        char                    szDescription[WSADESCRIPTION_LEN+1];
        char                    szSystemStatus[WSASYS_STATUS_LEN+1];
#endif
} WSADATA, FAR * LPWSADATA;
  • SOCKADDR_IN 结构体
typedef struct sockaddr_in {
    USHORT sin_port;
    IN_ADDR sin_addr;
    CHAR sin_zero[8];
} SOCKADDR_IN;
  • 服务端需要 bind 的原因

无连接(connect)的服务端、客户端和面向连接的服务端通过 bind 来配置本地信息;而有连接的客户端通过调用 connect 函数在socket 数据结构中保存本地和远端信息,不需要调用 bind()。

  • 需要初始化 WASStartup()的原因

之所以需要初始化winsocket,是因为Winsock的服务是以动态连接库Winsock DLL形式实现的,所以必须先调用初始化函数(WSAStartup)对Winsock DLL进行初始化,协商Winsock的版本支持,并分配必要的资源; // 在Linux环境中不需要该初始化步骤。

数据传输

在建立起连接的基础上,发送数据可以用接口 send / WSASend,接收数据可以用 recv / WSARecv

  • 对于 send 而言,发送数据的长度一般有限制,因为缓冲区或者 TCP/IP 的窗口大小有所限制,所以需要根据窗口大小来设定发送数据的长度。
  • 对于 recv 而言,流套接字是一个不间断的数据流,在读取它时,应用程序通常不会关心应该读取多少数据,如果所有消息长度都一样,这应该简单处理,如读取 1024 字节。
char recvBuff[2048];
int ret;    // 读取的数据长度
int nLeft;  // 剩余空间
int idx;    // 缓冲区数组下标
nLeft = 1024;
idx = 0;
while (nLeft > 0)
{
    ret = recv(socket1, &recvBuff[idx], nLfet, 0);
    if (ret == SOCKET_ERROR){
        // error 读取失败
        std::cout << "Error when receive message.";
    }
    idx += ret;
    nLeft -= ret;
}

  • 如果接收的消息长度不同,则按照发送端的协议来通知接收端,告知接收端即将到来的消息长度多少;比如,在消息的前几个字节设定标记,表示数据长度。

关闭连接

数据传输完成,关闭套接字,释放资源。

shutdown();    // 中断连接
closeSocket(socket_name);
WASCleanup();  // 释放 dll 

符号解释

WSAStartup

初始化 DLL,加载 socket,在Windows中,socket 以 dll 形式实现,dll 内部有一个计数器,第一次调用是真正加载 dll,后面再次调用 WSAStartup 是计数器加 1 ;与 WSAStartup 绑定使用的是 WSACleanup(),相反的,该函数只有最后一次调用才是真正卸载 dll,释放资源,前面的每次调用都是计数器减 1。

WSAStartup() 定义如下

int WSAAPI WSAStartup(              // WSAStartup 结构体
    _In_ WORD wVersionRequested,    // 高字节:指出副版本号,低字节:主版本号
    _Out_ LPWSADATA lpWSAData       // 指向win socket 实现的细节
    );

sin_family

表示地址家族。使用 TCP/IP 协议的应用程序必须设置 AF_INET,来告诉系统使用 IP 地址家族 。

sin_port

指定服务的端口号。1024–49151范围内的数据被作为服务端口号,可以由用户自定义。 sin_zero字段作为填充字段。以便使得该结构与SOCKADDR结构长度相同。

inet_addr

把本机IP的主机字节序转化为网络字节序。

该接口已经弃用,采用 inet_pton 或其他代替。

htonl / htons

host to net long/short htonl 和 htons 函数实现主机字节顺序和网络字节序的转换功能。H代表host,主机。N代表net,L代表long,S代表short。不能使用htonl转换short。

同理,网络字节序—> 主机字节序 :ntohl 、ntohs

af

表示协议使用的地址家族,创建TCP或UDP的套接字时使用AF_INET 地址家族。

type

socket套接字类型,有三种套接字类型:SOCK_STREAM / SOCKET_DGRAM / SOCK_ARM,分别表示数据流,数据包,原始套接字。

C/S 通信

ClientServer 要进行通信,首先需要建立连接;而客户端要连上服务端,首先需要开启服务端 Serverbind好服务端的IP地址和端口port,并设置监听listen,这时才能运行客户端程序,连接 上服务端,进行数据传输。

在Windows下进行的C/S通信,需要开启两个编译器分别编译运行 server.cpp 和 client.cpp,一般都有用 vs 吧,那就同时开启两个 vs ,不要将客户端和服务端程序写在同一个项目中。

以上仅为个人所学所得,仅供参考,欢迎不吝指正。

你可能感兴趣的:(网络编程)