一、基本概念介绍

首先我们来了解一下网络编程的概念,这点很多人其实都容易和网络原理搞混(网络原理通俗点说,就是谢希仁老师的计算机网络教材介绍的那些,也就是考研408中的计算机网络,但是网络编程是另一回事)。

网络原理主要研究网络的基本概念、协议、体系结构和工作原理。它研究计算机网络的组成部分、网络层次结构、数据传输和路由、网络拓扑、网络协议等方面的知识。网络原理的目标是理解网络的基本工作原理和设计,以便更好地理解和解决网络问题。

网络编程:使用编程语言和工具来实现网络通信和应用的开发过程。它涉及使用编程语言和网络库来创建网络连接、发送和接收数据、处理网络协议等。网络编程的目标是通过编写代码来实现网络应用,如客户端-服务器应用、分布式系统、Web应用等。

显然一个偏向理论,一个偏向实践,如果你有网络编程的基础,再去学习网络原理(计算机网络),你会发现学习起来无比轻松,就像发现了一个新世界一样!。

本篇博客主要介绍使用Windows提供的套接字API实现C++的原生态网络编程不引入其他第三方网络编程框架),实际上C++进行网络编程的方式实在是太多了,Windows提供的网络编程API我们称为Winsock API,这个API是不跨平台的只能在Windows进行使用,对应的BSD API就是Linux操作系统提供的C++原生态网络编程API,只能在Linux上进行使用,其他的第三方库基本支持跨平台的网络编程,比如boost.asio、Qt、pocol等

在进行编程之前,我们要了解几个基本的概念。

套接字(Socket):网络编程中的一种抽象概念,用于表示网络连接的一端。套接字可以是服务器套接字(用于监听和接受连接请求)或则客户端套接字(用于建立连接和发送请求),它提供了应用程序与网络传输层之间的通信能力。

端口(Port):用于标识网络通信中的特定应用程序或者服务的逻辑地址(进程和服务),每个网络通信的端口都有一个唯一的端口号与之关联,有效的端口号是0到65535,其中0到1023是被保留的端口号,用于一些特定的服务。

套接字和端口号是我们进行网络编程的核心,我们需要通过这两个来与不同主机进行通信。

网络通信模式:

1.客户端-服务器模型(C/S)

客户端发送请求,服务器端接受并响应请求,客户端和服务器可以在不同的主机上运行,典型的就是桌面应用程序,比如大家使用的百度网盘,大家安装在自己电脑上的就是客户端,大家可以通过客户端发送下载文件的请求(按下下载按钮),百度官方的服务器接收到请求,再做出响应,大家就可以下载对应的文件了。(本次博客主要实现这种方式)

2.浏览器-服务器模型(B/S)

浏览器作为客户端,通过发送HTTP请求与服务器进行通信。服务器接受并处理这些请求,并向浏览器发送HTTP响应,包含所需的数据或页面内容,典型的就是Web程序,Web应用程序可以直接通过浏览器连接到服务器,而不需要进行网络编程。

3.对等模式(P2P)

计算机或设备之间直接通信,无需依赖中央服务器,在P2P模式中,每个节点都可以充当客户端和服务器的角色,都可以提供服务和请求服务,这种模式一般用于文件共享、实时语音/视频通信和分布式计算等应用。

如果有同学学过计网的话,对上面的知识点应该还是比较熟悉的。

二、基本数据类型介绍

sockaddr和sockaddr_in(Socket Address Internet)

用于存储网络地址信息,包括协议族类型、IP地址和端口号,其实就是两个结构体用来设置服务端和客户端的IP地址和端口号的,我们最常使用的是sockaddr_in,操作系统内部使用的则是sockaddr

SOCKET

用于生成一个未初始化的套接字类型。

WSADATA(Windows Sockets Data)

用于初始化和清理Winsock库的信息结构,与WSADATA配套的函数主要就是WSACleanup();

WSACleanup()会释放Winsock库所占用的资源,并通知系统不再需要Winsock动态连接库函数。

三、基本步骤介绍

C++进行网络编程的方式其实相当多,基于WindowsAPI的实际上算是比较原始的一种了,很多人都是使用boost、poco等第三方库提供的API进行C++网络编程,不过对于网络编程的初学者来说,WindowsAPI的方式是比较好的一个锻炼方式(这篇博客主要针对TCP型的套接字进行网络编程)

3.1包含头文件

我们进行任何C++编程的第一步就是配置环境对吧,基于WindowsAPI进行网络编程并不需要什么复杂的配置过程,你只需要在代码的最上面加上这两行就可以了。

#include<winSock2.h>
#pragma comment(lib,WS2_32)

winSock2是Windows操作系统提供的头文件,用于进行网络编程,它包含了一些定义、宏和函数声明,用于操作套接字(sockets)和网络相关的功能,在使用C++进行Windows网络编程时,需要包含winsock2.h,并链接相应的库文件(比如ws2_32.lib)才能使用Winsock2的相关功能。

3.2初始化网络库

要使用我们上面所包含的库中的函数和宏等对象,需要将这个库进行初始化。

WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)//等于0代表初始化成功
{
std::cerr<<"Failed to initialize Winsock"<<std::endl;
return 1;
}

WSAStartup用初始化Winsock这个网络库,指示应用程序使用特定版本的Winsock,makeword()是一个宏,用于将两个字节合并成一个字(16位的无符号整数),指定Winsock库的版本。

3.3创建套接字

socket函数用于创建套接字,返回一个套接字SOCKET

SOCKET socket(int af, int type, int protocol);
//socket(协议族,套接字类型,协议)

协议族(如AF_INET表示IPv4)、套接字类型(如SOCK_STREAM表示TCP套接字)、协议(通常为0,表示自动选择合适的协议)。 

SOCKET listenSocket=socket(AF_INET,SOCK_STREAM,0);
if (listenSocket == INVALID_SOCKET) {
        std::cerr << "Failed to create socket." << std::endl;
        WSACleanup();
        return 1;
    }

 INVALID_SOCKET是一个宏,通常定义为-1,在Winsock中表示无效的套接字句柄值。

3.4绑定套接字(服务器函数)

bind函数将套接字绑定到特定的IP地址和端口号。

int bind(SOCKET s, const struct sockaddr *name, int namelen);
//bind(套接字描述符,sockaddr结构体指针,结构体大小)
//设置服务器地址和端口
sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
// 设置端口号 6000
    serverAddr.sin_port = htons(6000);  
//这个端口号与后面客户端连接的端口号是一样的
    serverAddr.sin_addr.s_addr = INADDR_ANY;

    // 绑定套接字到服务器地址和端口号
     if (bind(listenSocket, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) {
        std::cerr << "Failed to bind socket." << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
//reinterpret_cast<sockaddr*> 是 C++ 中的类型转换操作符,用于将一种指针类型转换为另一种指针类型

INADDR_ANY是一个宏,用于表示服务器程序在调用bind()函数绑定套接字时,可以绑定到主机上的任何可用IP地址 

3.5建立监听状态

listen函数将套接字设置为监听状态,等待客户端的连接请求

int listen(SOCKET s, int backlog);
//listen(套接字描述符,等待连接的最大数量)
 if (listen(listenSocket, 5) == SOCKET_ERROR) {
        std::cerr << "Failed to listen on socket." << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

3.6等待接收

accept函数接受客户端得连接请求,并创建一个新的套接字来与客户端进行通信

SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen);
//accept(套接字描述符,sockaddr结构体指针,结构体大小)
SOCKET clientSocket = accept(listenSocket, nullptr, nullptr);
    if (clientSocket == INVALID_SOCKET) {
        std::cerr << "Failed to accept client connection." << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
//clientSocket位于服务器端,用于与对应的客户端进行通信

这里之所以命名为clientSocket,主要是因为,服务器端的serverSocket本身并不处理客户端的请求,而是会再额外创建一个套接字(也就是clientSocket)去处理客户端请求。

3.7接收数据

recv函数用于接收数据。

int recv(SOCKET s, char *buf, int len, int flags);
//recv(套接字描述符,要接受数据的缓冲区指针,缓冲区长度,接受操作的方式)
char buffer[1024];
    int bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
    if (bytesRead == SOCKET_ERROR) {
        std::cerr << "Failed to receive data." << std::endl;
        closesocket(clientSocket);
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    buffer[bytesRead] = '\0';

将 int flags 参数设置为0表示不使用任何特殊的接收标志(默认做法)。

3.8发送数据

send函数用于发送数据。

int send(SOCKET s, const char *buf, int len, int flags);
//send(套接字描述符,要发送数据的缓冲区的指针,数据长度,接受操作的方式)
std::string message = "Hello, client!";
    if (send(clientSocket, message.c_str(), message.size(), 0) == SOCKET_ERROR) {
        std::cerr << "Failed to send data." << std::endl;
        closesocket(clientSocket);
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }

3.9连接服务器(客户端函数)

connect函数用于客户端与远程服务器建立连接。

int connect(SOCKET s, const struct sockaddr *name, int namelen);
connect(套接字描述符,sockaddr指针,结构体大小)
// 创建套接字
    SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
   
// 设置服务器地址和端口
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(6000);//与服务器端口号保持一致
if (inet_pton(AF_INET, "你主机的IP地址", &(serverAddr.sin_addr)) <= 0)//IP地址可以用ipconfig指令查看
{
//#include <WS2tcpip.h> // 添加这个头文件
	std::cerr << "无效的服务器地址" << std::endl;
	closesocket(clientsocket);
	WSACleanup();
	return 1;
}
    //过时的写法://serverAddr.sin_addr.s_addr = //inet_addr("你主机的IP地址");

    // 连接服务器
    if (connect(sockfd, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == SOCKET_ERROR) {
        std::cerr << "Failed to connect to server." << std::endl;
        closesocket(sockfd);
        WSACleanup();
        return 1;
    }

3.10关闭套接字

closesocket函数用于关闭套接字

int closesocket(SOCKET s);
closesocket(套接字描述符)
closesocket(clientSocket);
    closesocket(listenSocket);
    WSACleanup();//调用WSACleanup()释放Winsock所占用的资源,并通知系统不再需要Winsock动态连接库函数

四、实战环节

4.1服务器端代码

// server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include<WinSock2.h>
#include <WS2tcpip.h> // 添加这个头文件
#pragma comment(lib,"WS2_32")
int main()
{
    std::cout << "this is Server windows" << std::endl;
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        std::cerr << "Failed to initialize Windsock" << std::endl;
        return 1;
    }
    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == INVALID_SOCKET)
    {
        std::cerr << "Failed to create socket" << std::endl;
            WSACleanup();
            return 1;
    }
    sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(6000);
    if (inet_pton(AF_INET, "我主机的IP地址", &(serverAddr.sin_addr)) <= 0)
    {
        std::cerr << "无效的服务器地址" << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    if (bind(listenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
    {
        std::cerr << "Failed to bind socket." << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    if (listen(listenSocket, 5) == SOCKET_ERROR)
    {
        std::cerr << "Failed to listen onf socket." << std::endl;
        closesocket(listenSocket);
        WSACleanup();
        return 1;
    }
    SOCKET clientSocket = accept(listenSocket, nullptr, nullptr);
    while (true)
    {
        if (clientSocket == INVALID_SOCKET)
        {
            std::cerr << "Failed to accept client connection" << std::endl;
            closesocket(listenSocket);
            WSACleanup();
            return 1;
        }

        char revBuffer[1024]="";
        if (recv(clientSocket, revBuffer, sizeof(revBuffer), 0) == SOCKET_ERROR)
        {
            std::cerr << "Failed to receive data" << std::endl;
            closesocket(clientSocket);
            continue;
        }
        std::cout << "recive client:" << revBuffer << std::endl;
        std::string message = "server:hello client";
        if (send(clientSocket, message.c_str(), message.size(), 0) == SOCKET_ERROR)
        {
            std::cerr << "Failed to send data" << std::endl;
        }
    }
    closesocket(clientSocket);
    closesocket(listenSocket);
    WSACleanup();
    return 0;
}

4.2客户端代码

#include <iostream>
#include<WinSock2.h>
#include <WS2tcpip.h> // 添加这个头文件
#include<string>
#pragma comment(lib,"WS2_32")
int main()
{
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		std::cerr << "初始化网络库失败" << std::endl;
		WSACleanup();
		return 1;
	}
	SOCKET clientsocket;
	if ((clientsocket=socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET)
	{
		std::cerr << "创建套接字失败" << std::endl;
		closesocket(clientsocket);
		WSACleanup();
		return 1;
	}
	sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(6000);
	if (inet_pton(AF_INET, "我主机的iP地址", &(server_addr.sin_addr)) <= 0)
	{
		std::cerr << "无效的服务器地址" << std::endl;
		closesocket(clientsocket);
		WSACleanup();
		return 1;
	}
	if (connect(clientsocket, (sockaddr*)&server_addr, sizeof(sockaddr)) == SOCKET_ERROR)
	{
		std::cerr << "连接到服务器端失败" << std::endl;
		closesocket(clientsocket);
		WSACleanup();
		return 1;
	}
	while (true)
	{
		std::string message;
		std::cout << "Enter a message to send (or 'quit' to exit): ";
	std::getline(std::cin, message);
		// Check if the user wants to quit
		if (message == "quit")
		{
			break;
		}

		if (send(clientsocket, message.c_str(), message.size(), 0) == SOCKET_ERROR)
		{
			std::cerr << "Failed to send data" << std::endl;
			closesocket(clientsocket);
			WSACleanup();
			return 1;
		}

		char revBuffer[1024]="";
		if (recv(clientsocket, revBuffer, sizeof(revBuffer), 0) == SOCKET_ERROR)
		{
			std::cerr << "Failed to receive data" << std::endl;
			closesocket(clientsocket);
			WSACleanup();
			return 1;
		}

		std::cout << "Received message from server: " << revBuffer << std::endl;
	}
		closesocket(clientsocket);
	WSACleanup();
	return 0;
}

4.3注意点

1.上面使用到的字符数组必须初始化为空,而且不能用string类型,不然会中文乱码。

 2.上面代码的IP地址,我都没有替换掉,所以你们要替换成为你们的IP地址,IP地址的查看可以使用ipconfig指令在控制台进行查看,具体也可以看看其他人的教程。

3.必须先运行服务器的程序,再运行客户端的程序。

4.服务器程序和客户端程序是两个项目!!!,不要写在一起,要可以分开运行。

4.4运行效果

服务器

客户端

在客户端窗口输入"你好,服务器,我是客户端",发现服务器窗口接收到了这个消息并打印在了窗口,同时服务器在受到这个消息后,向客户端做出回应,发出了"hello client",客户端也成功接收到了这个消息,并打印到窗口上面,成功实现了两个程序之间的通信。 

 接下来我们再运行五次客户端程序看看会发生什么,前面的窗口不要关!!!

一直到第五个窗口,我们发现并没有报错,说明五个窗口都连接到了服务器上了。

 如果是第六个窗口呢,直接报错,程序结束,没有成功连接到服务器,这是因为我们服务器设置的listen函数最大连接数是5,而前面五个窗口都没有结束,所以第六个自然就连不上了。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐