小白劝退预告

  • 仅简单介绍思路,没有用作教程的打算,如果读者没有C++编程基础、计算机网络基础、网络编程基础或并发编程基础 —— 会很不友好的(

Web服务器概述

Web服务器概念

服务器是一种软件应用程序,用于处理客户端发送的HTTP请求并向客户端发送对应的响应。它有时通过使用网络协议(如HTTP)来传输和交换Web资源(如HTML文档、图像、视频等)
服务器是当代互联网不可分割的一部分,倘若没有服务器,便无谈上网冲浪; 每次你打开一个网页,背后的Web服务器时时刻刻都在处理你的请求,并发送给你需要的文本数据和图片; 当你打游戏时,科技公司的服务器在马不停蹄地为你计算数据

Web服务器的代码思路

在C++中实现一个简单的服务器,可以按照以下思路进行:

  1. 创建服务器套接字:使用C++的网络编程库,如Linux环境下的 <sys/socket.h><netinet/in.h> 等、windows环境下载Wsa等,创建一个服务器套接字
  2. 绑定服务器地址和端口:将服务器套接字绑定到指定的IP地址和端口上,以便监听客户端请求
  3. 监听连接请求:使用listen函数开始监听客户端连接请求,设置最大连接数
  4. 接受连接请求:使用accept函数接受客户端的连接请求,返回一个新的套接字,用于与客户端进行通信

上述操作能实现一个联网的简单服务器;本文旨在编写一个简单的服务器框架

C++ 实现多线程服务器

以下内容涉及到C++的面向对象编程、网络编程和并发编程

实现基本联网功能

定义Basic_server对象

我们可以采用过程化的语言来编写我们的服务器,在这里我采用面向对象的思路来编写
我们先定义一个Basic_server类,实现最基本的联网功能

现在分别解释说明Basic_server对象里各成员的作用:

  • isopen 是一个布尔值,指示了当前服务器的运行状态,开启时为true,关闭时为false
  • sock 是这个服务器内置的一个套接字
  • port 是指定了这个服务器进程占用的端口号
  • task 是一个函数指针,它接受一个SOCKET对象作为参数,当服务器运行时,交互逻辑实现于task所指向的函数对象
  • void init() 是一个初始化函数,配置了网络编程所需要的资源
  • SOCKET get_socket() 是一个安全的获取socket的函数,若获取失败,则会关闭服务器(设置isopenfalse)
  • virtual sockaddr_in get_addr() 是一个安全的获取sockaddr_in的函数,定义为纯虚函数,后续继承时重写
  • Basic_server() 构造函数,用于初始化
  • ~Basic_server() 析构函数,销毁资源
  • void bind_task(void(*_task)(SOCKET sock)) 绑定任务的函数,用于修改task指针指向的函数地址
  • virtual void run() 让服务器运行的函数,定义为纯虚函数,后续继承时重写
  • void close() 关闭服务器的函数,并设置isopenfalse
  • is_open() 返回当前服务器运行状态的函数
    Basic_server.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Basic_server{
    protected:
    bool isopen;
    SOCKET sock;
    int port;
    void (*task)(SOCKET sock) = nullptr;

    void init();
    SOCKET get_socket();
    virtual sockaddr_in get_addr()=0;

    public:
    Basic_server(int port);
    ~Basic_server();

    void bind_task(void(*_task)(SOCKET sock));
    virtual void run() = 0;
    void close();
    bool is_open();
    };

Basic_server的构造函数

首先我们这个服务器是确定在Windows环境编写的,最终这个程序也只能运行在Windows环境,换到别的地方(比如Linux环境)就无法运行 —— 因为Linux不支持Windows提供的系统API
当然,现实生活中的Web服务器大多数都是运行在Linux环境里的,等以后有时间再来做修改伐~

  1. 先实现init()函数,配置好对应的网络编程环境资源

    Basic_server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <WinSock2.h>
    #pragma comment(lib,"ws2_32.lib")

    void Basic_server::init() {
    WORD sockVersion = MAKEWORD(2, 2);
    WSADATA wsaData;

    if (WSAStartup(sockVersion, &wsaData) != 0) {
    cout << "WSAStartup() error!" << endl;
    close();
    }
    return;
    }
  2. 实现get_socket()函数

    Basic_server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SOCKET Basic_server::get_socket() {
    if (!isopen)
    return SOCKET();

    SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (sock == INVALID_SOCKET) {
    cout << "socket error !" << endl;
    close();
    }
    return sock;
    }
  3. 最终实现Basic_server()构造函数

    Basic_server.cpp
    1
    2
    3
    4
    5
    6
    7
    Basic_server::Basic_server(int _port){
    init();
    isopen = true;
    port = _port;
    sock = get_socket();
    return;
    }

Basic_server的析构函数

析构函数主要有以下的作用:

  • 关闭服务器,若服务器已经因为故障而中途关闭,则无需再次关闭
  • 销毁创建的Windows网络编程环境
  1. 先实现close()函数,帮我们关闭服务器

    Basic_server.cpp
    1
    2
    3
    4
    5
    6
    void Basic_server::close() {
    isopen = false;
    closesocket(sock);
    WSACleanup();
    return;
    }
  2. 最后实现析构函数

    Basic_server.cpp
    1
    2
    3
    4
    5
    Basic_server::~Basic_server(){
    if (isopen)
    close();
    return;
    }

Basic_server的工具函数

工具函数主要就是提供辅助功能的函数,大多用于修改或获取类的对象

  1. 实现bind_task(void(*_task)(SOCKET sock))函数

    Basic_server
    1
    2
    3
    4
    void Basic_server::bind_task(void(*_task)(SOCKET sock)){
    task = _task;
    return;
    }
  2. 实现is_open()函数

    Basic_server
    1
    2
    3
    bool Basic_server::is_open(){
    return isopen;
    }

实现基本并发功能

定义 Server 对象

Server 对象继承自 Basic_server,在其基本的联网功能上再附带了端口监听多线程处理任务的功能,能实现基本并发服务器的功能

现在分别解释说明 Basic_server 对象里各成员的作用:

  • listen_num 定义了该服务器最大的同时监听数量
  • sockaddr_in addr 定了该服务器进程绑定的ip端口
  • sockaddr_in get_addr() 一个安全获取sockaddr_in对象的函数,若故障则服务器关闭
  • void bind_socket()sock成员与addr成员绑定,若故障则服务器关闭
  • static void do_accept(void* ptr) 服务器的接收请求、并处理请求的函数,因多线程的特殊性,定义为静态成员函数,为了能访问到类成员,需要传入一个this指针
  • Server(int port, int num) 构造函数,同时完成套接字与通信地址(ip与端口)的绑定
  • ~Server() 析构函数,释放资源
  • void run() 让服务器最终运行的函数
    Server.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Server : public Basic_server {
    private:
    int listen_num;
    sockaddr_in addr;

    protected:
    sockaddr_in get_addr();
    void bind_socket();
    void listen_socket();
    static void do_accept(void* ptr);

    public:
    Server(int port, int num);
    ~Server();

    void run();
    };

Server的构造函数

  1. 先实现sockaddr_in get_addr()函数,它依据端口号返回一个通信地址

    Server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    sockaddr_in Server::get_addr() {
    if (!isopen)
    return sockaddr_in();

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.S_un.S_addr = ADDR_ANY;
    return addr;
    }
  2. 再实现void bind_socket()函数,它负责将套接字与通信地址绑定

    Server.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Server::bind_socket() {
    if (!isopen)
    return;

    if (bind(sock, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) {
    printf("bind error.\n");
    close();
    }
    return;
    }
  3. 最后实现Server(int port, int num)构造函数

    Server.cpp
    1
    2
    3
    4
    5
    6
    Server::Server(int _port, int num) :Basic_server(_port) {
    listen_num = num;
    addr = get_addr();
    bind_socket();
    return;
    }

Server的析构函数

  1. 非常简单粗暴的析构函数
    Server.cpp
    1
    2
    3
    Server::~Server() {
    return;
    }

Server的运行函数

  1. 实现void listen_socket(),开始监听端口

    Server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Server::listen_socket() {
    if (!isopen)
    return;

    if (listen(sock, listen_num) == SOCKET_ERROR) {
    printf("listen error.\n");
    close();
    }
    return;
    }
  2. static void do_accept(void* ptr),用于执行监听的交互处理
    交互处理的核心在于task指针所指向的函数地址

    Server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void Server::do_accept(void* ptr){
    Server* server = (Server*)ptr;
    while (server->isopen) {
    SOCKET cli;
    sockaddr_in cli_addr;
    int addr_len = sizeof(cli_addr);

    printf("accept.\n");
    cli = accept(server->sock, (sockaddr*)&cli_addr, &addr_len);
    if (cli == INVALID_SOCKET) {
    printf("accept error.\n");
    continue;
    }
    printf("connect to %s.\n", inet_ntoa(cli_addr.sin_addr));

    server->task(cli);

    printf("connect close.\n");
    }
    }
  3. void run(),封装了所有的服务器运行处理
    在这个函数中,我们先对端口监听,并开辟了128个子线程,用来处理服务器与外界的交互
    对于线程的处理,我们可以修改成动态线程池,能大大优化服务器的性能,后续有空再做伐~

    Server.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <thread>
    void Server::run() {
    listen_socket();
    if (!isopen || task == nullptr) {
    printf("no task to excuate.\n");
    return;
    }

    thread ths[128];
    for (auto& th : ths)
    th = move(thread(do_accept, this));

    for (auto& th : ths)
    th.join();

    return;
    }

服务器检测

实现Echo服务器

至此,我们已经实现了一个并发服务器的框架,这个服务器的框架已经就绪,使用起来也极其方便:你只需提前写好一个task函数,将服务器对象与这个函数绑定即可。接下来,我们就依照我们写好的服务器框架来实现一个Echo服务器,用于我们框架的检测

Echo服务器又称复读服务器,即客户端发送什么内容给服务器,服务器就返回什么内容给客户端

  1. 编写task函数

    main.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void task_echo(SOCKET sock) {
    while (true) {
    char send_buf[255], recv_buf[255];
    int len = recv(sock, recv_buf, 255, 0);
    if (len > 0)
    printf("Client: %s\n", recv_buf);
    else {
    printf("Client close.\n");
    break;
    }

    strcpy(send_buf, recv_buf);
    send(sock, send_buf, 255, 0);
    printf("Server: %s\n", send_buf);
    }
    return;
    }
  2. 在主程序中创建Server对象,绑定task并运行

    main.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
    Server server(8888, 128);
    server.bind_task(task_echo);
    server.run();
    server.close();

    return 0;
    }

    由此,你会发现我们采用面向对象的思想封装服务器对象的简洁性、以及使用时的高效性
    依靠API即可便捷运行,这就是OOP!(虽然编写底层代码时很要命/bushi)

编写客户端程序

  1. 依旧采用OOP的思想来编写客户端,定义一个Client对象继承自Basic_server
    下面依次解释说明每个类成员的含义和作用:
  • ip 存储了客户端连接的服务器ip,采用字符串存储
  • addr 定义了客户端要连接的通信地址
  • sockaddr_in get_addr() 获取通信地址的工具函数
  • client_connect() 让客户端与服务器连接
  • Client(const char* ip, int port) 构造函数
  • ~Client() 析构函数
  • void run() 让客户端开始运行的函数
    Client.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Client : public Basic_server {
    private:
    char ip[20];
    sockaddr_in addr;

    protected:
    sockaddr_in get_addr();
    void client_connect();

    public:
    Client(const char* ip, int port);
    ~Client();

    void run();
    };
  1. 接下来是客户端的代码实现,这里就不过多解释了

    Client.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    void Client::client_connect() {
    if (!isopen)
    return;

    int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
    printf("connect to %s:%d ", ip, port);
    if (ret == -1) {
    isopen = false;
    printf("error.\n");
    }
    else
    printf("success.\n");
    return;
    }

    sockaddr_in Client::get_addr() {
    if (!isopen)
    return sockaddr_in();

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.S_un.S_addr = inet_addr(ip);
    return addr;
    }

    Client::Client(const char* _ip, int _port) :Basic_server(_port) {
    strcpy(ip, _ip);
    addr = get_addr();
    return;
    }

    Client::~Client() {
    return;
    }

    void Client::run() {
    client_connect();

    if (!isopen || task == nullptr) {
    printf("no task to excuate.\n");
    return;
    }

    task(sock);

    printf("connect close.\n");

    return;
    }

    Server对象类似,Client提供了一个框架,具体的交互功能需要开发者自己去编写task函数并绑定后才可运行

  2. 编写task函数,实现Echo客户端

    Client.cpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    void task_echo(SOCKET sock) {
    while (true) {
    char send_buf[255], recv_buf[255];
    while(cin.getline(send_buf, 255))
    if(strlen(send_buff) > 0)
    break;

    send(sock, send_buf, sizeof(send_buf), 0)
    printf("Client: %s\n", send_buf);

    int len = recv(sock, recv_buf, 255, 0)
    if(len > 0){
    printf("Server: %s\n", recv_buf);
    }
    else{
    printf("connect loss.\n");
    break;
    }
    }
    printf("Client close.\n");

    return;
    }

    int main(){
    Client client("127.0.0.1", 8888);
    client.run();
    client.close();
    return 0;
    }

检测

  1. 单个客户端连接检测
  • 运行我们的服务器程序,可以看到它正在监听
  • 运行我们的客户端程序,发现连接成功
  • 在客户端发送任意消息,发现服务器可以正常地返还相同的数据
    至此,检测成功
    Echo1
  1. 多个客户端连接并发检测
  • 操作过程与 (1) 类似,不过同时打开很多个窗口
  • 发现每个客户端都可以单独与服务器交互,服务器也可以同时与多个客户端联通
    至此,检测成功
    Echo2

附录代码

基本并发服务器框架

采用多文件编写,两个头文件Basic_server.hTokis,h,两个cpp文件Basic_serverTokis.cpp

Basic_server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<WinSock2.h>
using namespace std;
#pragma comment(lib,"ws2_32.lib")

class Basic_server{
protected:
bool isopen;
SOCKET sock;
int port;
void (*task)(SOCKET sock) = nullptr;

void init();
SOCKET get_socket();
virtual sockaddr_in get_addr()=0;

public:
Basic_server(int port);
~Basic_server();

void bind_task(void(*_task)(SOCKET sock));
virtual void run() = 0;
void close();
bool is_open();
};
Tokis.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#pragma once
#include "Basic_server.h"
#include <thread>

class Server : public Basic_server {
private:
int listen_num;
sockaddr_in addr;

protected:
sockaddr_in get_addr();
void bind_socket();
void listen_socket();
static void do_accept(void* ptr);

public:
Server(int port, int num);
~Server();

void run();
};


class Client : public Basic_server {
private:
char ip[20];
sockaddr_in addr;

protected:
void client_connect();
sockaddr_in get_addr();

public:
Client(const char* ip, int port);
~Client();

void run();
};
Basic_server.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "Basic_server.h"

void Basic_server::init() {
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;

if (WSAStartup(sockVersion, &wsaData) != 0) {
cout << "WSAStartup() error!" << endl;
close();
}
return;
}

SOCKET Basic_server::get_socket() {
if (!isopen)
return SOCKET();

SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
cout << "socket error !" << endl;
close();
}
return sock;
}

Basic_server::Basic_server(int _port){
init();
isopen = true;
port = _port;
sock = get_socket();
return;
}

Basic_server::~Basic_server(){
if (isopen)
close();
return;
}

void Basic_server::bind_task(void(*_task)(SOCKET sock)){
task = _task;
return;
}

void Basic_server::close() {
isopen = false;
closesocket(sock);
WSACleanup();
return;
}

bool Basic_server::is_open(){
return isopen;
}
Tokis.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#include "Tokis.h"

void Server::bind_socket() {
if (!isopen)
return;

if (bind(sock, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) {
printf("bind error.\n");
close();
}
return;
}

void Server::listen_socket() {
if (!isopen)
return;

if (listen(sock, listen_num) == SOCKET_ERROR) {
printf("listen error.\n");
close();
}
return;
}

void Server::do_accept(void* ptr){
Server* server = (Server*)ptr;
while (server->isopen) {
SOCKET cli;
sockaddr_in cli_addr;
int addr_len = sizeof(cli_addr);

printf("accept.\n");
cli = accept(server->sock, (sockaddr*)&cli_addr, &addr_len);
if (cli == INVALID_SOCKET) {
printf("accept error.\n");
continue;
}
printf("connect to %s.\n", inet_ntoa(cli_addr.sin_addr));

server->task(cli);

printf("connect close.\n");
}
}

sockaddr_in Server::get_addr() {
if (!isopen)
return sockaddr_in();

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.S_un.S_addr = ADDR_ANY;
return addr;
}

Server::Server(int _port, int num) :Basic_server(_port) {
listen_num = num;
addr = get_addr();
bind_socket();
return;
}

Server::~Server() {
return;
}

void Server::run() {
listen_socket();
if (!isopen || task == nullptr) {
printf("no task to excuate.\n");
return;
}

thread ths[128];
for (auto& th : ths)
th = move(thread(do_accept, this));

for (auto& th : ths)
th.join();

return;
}

void Client::client_connect() {
if (!isopen)
return;

int ret = connect(sock, (struct sockaddr*)&addr, sizeof(addr));
printf("connect to %s:%d ", ip, port);
if (ret == -1) {
isopen = false;
printf("error.\n");
}
else
printf("success.\n");
return;
}

sockaddr_in Client::get_addr() {
if (!isopen)
return sockaddr_in();

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.S_un.S_addr = inet_addr(ip);
return addr;
}

Client::Client(const char* _ip, int _port) :Basic_server(_port) {
strcpy(ip, _ip);
addr = get_addr();
return;
}

Client::~Client() {
return;
}

void Client::run() {
client_connect();

if (!isopen || task == nullptr) {
printf("no task to excuate.\n");
return;
}

task(sock);

printf("connect close.\n");

return;
}

Echo服务器和客户端

请确保你已经提前保存了上文基本并发服务器框架的代码
采用多文件编写,两个cpp文件server.cppclient.cpp

server.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma once
#include "Tokis.h"
#include <iostream>

using namespace std;

void task_echo(SOCKET sock) {
while (true) {
char send_buf[255], recv_buf[255];
int len = recv(sock, recv_buf, 255, 0);
if (len > 0)
printf("Client: %s\n", recv_buf);
else {
printf("Client close.\n");
break;
}

strcpy(send_buf, recv_buf);
send(sock, send_buf, 255, 0);
printf("Server: %s\n", send_buf);
}
return;
}

int main() {
Server server(8888, 128);
server.bind_task(task_echo);
server.run();
server.close();

return 0;
}
client.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma once
#include "Tokis.h"
#include <iostream>

using namespace std;

void task_echo(SOCKET sock) {
while (true) {
char send_buf[255], recv_buf[255];
while(cin.getline(send_buf, 255))
if(strlen(send_buff) > 0)
break;

send(sock, send_buf, sizeof(send_buf), 0)
printf("Client: %s\n", send_buf);

int len = recv(sock, recv_buf, 255, 0)
if(len > 0){
printf("Server: %s\n", recv_buf);
}
else{
printf("connect loss.\n");
break;
}
}
printf("Client close.\n");

return;
}

int main(){
Client client("127.0.0.1", 8888);
client.run();
client.close();
return 0;
}