👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
一、 服务端
1.1 前言
这里我们规定将TCP
服务器封装成一个类,以下是服务器程序框架
tcpServer.cc
#include <iostream>
#include "tcpServer.hpp"
#include <memory>
using namespace std;
int main()
{
// 1. 创建TCP服务器端对象
unique_ptr<tcpserver> tcpsvr(new tcpserver());
// 2. 初始化TCP服务器
tcpsvr.Init();
// 3. 启动TCP服务器
tcpsvr.Run();
return 0;
}
tcpServer.hpp
#pragma once
#include <iostream>
#include "log.hpp"
log lg;
class tcpserver
{
public:
tcpserver()
{
}
~tcpserver()
{
}
void Init()
{
}
void Run()
{
}
private:
};
说明:lg
是我往期封装的日志类对象,这个在UDP也使用过。具体可以参考一下文章:
1.2 创建套接字
要想进行网络通信,第一步就是要先创建套接字socket
。
【函数原型】
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
说明:
domain
:指定套接字的协议族(如AF_INET
表示IPv4
、AF_INET6
表示IPv6
网络、AF_UNIX / AF_LOCAL
用于本地进程间通信)。type
:指定套接字的类型(如SOCK_STREAM
表示TCP
,SOCK_DGRAM
表示UDP
)。protocol
:指定具体的协议,通常设置为 0 表示使用默认协议。
综上,TCP
服务器在创建套接字时,参数设置如下:
-
参数一:因为我们要进行的是网络通信,协议家族选择
AF_INET
。 -
参数二:因为我们编写的是
TCP
服务器,所以选择SOCK_STREAM
。 -
参数三:协议类型默认设置为
0
即可。
tcpServer.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include "log.hpp"
log lg;
enum
{
SOCK_ERR = 1,
};
class tcpserver
{
public:
tcpserver()
: _socketfd(-1)
{
}
~tcpserver()
{
if (_socketfd >= 0)
{
close(_socketfd);
}
}
void Init()
{
// 创建套接字
_socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (_socketfd == -1) // 套接字创建失败
{
lg.logmessage(Fatal, "create socket. errno: %d, describe: %s", errno, strerror(errno));
exit(SOCK_ERR);
}
lg.logmessage(Info, "create socket. errno: %d, describe: %s", errno, strerror(errno));
}
private:
int _socketfd; // 套接字文件描述符
};
说明:
-
实际
TCP
服务器创建套接字的做法与UDP
服务器是一样的,只不过创建套接字时TCP
需要的是流式服务,即SOCK_STREAM
;而UDP
需要的是用户数据报服务,即SOCK_DGRAM
。 -
当析构服务器时,可以将服务器对应的文件描述符进行关闭。也可以选择不关闭。因为操作系统会在进程结束时自动回收文件描述符。然而,为了避免资源泄露和潜在的文件描述符耗尽问题,最好在程序退出前手动关闭文件描述符。这样可以更好地控制资源的释放。
1.3 绑定套接字
套接字创建完毕后,实际只是在系统层面上打开了一个文件(网卡文件),该文件还没有与网络关联起来,因此创建完套接字后我们还需要进行绑定操作bind
。
【函数原型】
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
说明:
sockfd
:套接字的文件描述符,通常是由socket
函数返回的。addr
:sockaddr
结构体是一个套接字的通用结构体,实际我们在进行网络通信时,还是要定义sockaddr_in
和sockaddr_un
这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*
。sockaddr_in
结构体:用于跨网络通信sockaddr_un
结构体:是用于本地通信- 注意:使用以上结构体需要加上头文件
<netinet/in.h>
addrlen
:addr
结构体的大小,以字节为单位。- 返回值:成功返回
0
;失败返回-1
,并设置errno
以指示错误原因。
首先对于bind
函数,第一个参数和第三个参数没的说。主要是第二个参数。因为是跨网络通信,我们需要定义struct sockaddr_in
结构体。而该结构体具体有哪些成员呢?我们可以在vscode
中查看sockaddr_in
结构体的相关成员:
注意:因为该结构体中还有其他字段,我们可以不用管。因此,可以使用 memset
函数可以确保结构体的所有字段都被初始化为零,从而避免意外数据。
#include <string.h>
void *memset(void *s, int c, size_t len);
// s是指向要填充的内存区域的指针
// c是要填充的值
// len是要填充的字节数
// -------------------------
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 将 addr 结构体的所有字节设置为零
这里再介绍一个函数bzero
,它的作用是一个用于将内存区域的字节设置为零的函数。
#include <strings.h>
void bzero(void *s, size_t len);
初始化之后,我们需要设置struct sockaddr_in
成员变量,比如端口号和IP
地址之类的
sin_family
:表示协议家族。必须使用与socket
创建时相同的协议家族。例如,如果你使用AF_INET
创建了套接字,bind
时也应使用AF_INET
。sin_port
:表示端口号,是一个16
位的整数。注意:端口号需要转换为网络字节序。因为只要进行网络通信,端口号一定是双方来回传输的数据,因此为了保证双方能够正常解析数据,需要将其转换为网络字节序。可以使用htons
函数。
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
sin_addr
:其中sin_addr
的类型是struct in_addr
,实际该结构体当中就只有一个成员,该成员就是一个32
位的整数,就是IP
地址。我们用户最直观的就是输入类似于192.168.1.1
这种字符串形式的IP
地址,可是这里sin_addr
的类型是32
位无符号整数,那么我们要把字符串转化为32
位无符号整数。并且在网络通信中,IP
地址和端口号一样,也是双方来回传输的数据,也要保证双方能够正常解析数据。由于这些操作在网络中经常会用到,我们并不需要自己手动实现一遍,系统已经我们提供了一些函数:
对于IP
地址,我们可以先将其设置为本地环回127.0.0.1
来进行本地通信测试;当然也可以设置为公网IP
地址,表示网络通信。
但需要注意的是:
- 如果你使用的是虚拟机,那么可以设置为公网
IP
地址;如果使用的是云服务器,那么在设置服务器的IP
地址时,不需要显示绑定IP
地址,但可以设置成本地环回地址,当然也可以直接设置为INADDR_ANY
,这是个宏函数,本质上就是0.0.0.0
。此时服务器就可以从本地任何一张网卡当中读取数据。 - 另外,以上绑定只是在用户层上绑定了,填充完服务器网络相关的属性信息后,需要调用
bind
函数进行内核绑定。绑定实际就是将文件与网络关联起来,如果绑定失败也没必要进行后续操作了,直接终止程序即可。
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
log lg;
enum
{
SOCK_ERR = 1,
BIND_ERR
};
class tcpserver
{
public:
tcpserver(const uint16_t &port, const std::string &ip = "0.0.0.0")
: _socketfd(-1), _port(port), _ip(ip)
{
}
void Init()
{
// 1. 创建套接字
_socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (_socketfd == -1) // 套接字创建失败
{
lg.logmessage(Fatal, "create socket. errno: %d, describe: %s", errno, strerror(errno));
exit(SOCK_ERR);
}
lg.logmessage(Info, "create socket. errno: %d, describe: %s", errno, strerror(errno));
// 2. 绑定套接字
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port); // 细节
inet_aton(_ip.c_str(), &(local.sin_addr)); // 细节
// 内核绑定
int n = bind(_socketfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0) // 绑定失败
{
lg.logmessage(Fatal, "bind socket. errno: %d, describe: %s", errno, strerror(errno));
exit(BIND_ERR);
}
lg.logmessage(Info, "bind socket. errno: %d, describe: %s", errno, strerror(errno));
}
private:
int _socketfd; // 套接字文件描述符
uint16_t _port; // 服务器端口号
std::string _ip; // 服务器IP地址
};
代码写到目前为止,虽然细节很多,但其实和UDP
前半部分一样!都是套路!
而接下来就和UDP
不一样了,因为TCP
是一个面向连接的,所以在正式通信之前,一定要客户端和服务器连接上再说,即要将套接字设置为监听状态。因为只有设置为监听状态,才能知道有别人要来和我建立连接,然后基于连接再进行通信。
1.4 监听
listen
函数用于将套接字设置为监听状态,以等待传入的连接请求。它通常用于服务器端,配合 socket
和 bind
函数使用。
【函数原型】
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
说明:
sockfd
: 是之前通过socket
函数创建的套接字描述符。backlog
: 指定套接字的待连接队列的最大长度(数目)。意思就是说,如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度。注意:一般不要设置太大,设置为5
或10
即可。- 返回值:成功返回
0
,失败返回-1
,并设置errno
以指示错误类型。
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>