Muduo网络库实现 [三] - Socket模块
目录
设计思路
类的设计
模块的实现
基础模块
特殊模块
集成模块
主函数
主函数实现
主函数测试
疑惑点
设计思路
Socket模块主要是对套接字的基础操作进行封装,简化我们对套接字的操作,不需要调用C的原生接口,而是以面向对象的方式来调用。
那么我们需要封装哪些接口呢?
- 首先,最基础的接口,创建套接字,绑定地址信息,建立连接,开始监听,获取新连接,读取数据,写入数据,关闭套接字这几个基本功能我们还是需要提供的。
- 其次就是两个特殊的功能: 设置套接字非阻塞,因为后续我们读取和写入都是非阻塞进行的。 还有就是设置地址信息和端口号复用,这是为了便于服务器崩溃之后能够立即以固定端口重启。
- 还需要提供两个集成的功能,创建一个服务器连接,以及建立一个客户端连接。
类的设计
public:
/*-- - 基础功能-- -*/
bool Create();//创建套接字
bool Bind();//绑定地址信息
bool Connect();//向服务端发起连接
bool Listen();//服务端开始监听
int Accept();//获取客户端连接
ssize_t send();//发送数据
ssize_t Recv();//接收数据
void close();//关闭套接字
/*-- - 特殊功能-- -*/
ssize_t SendNonBlock(void *buf, size_t len) // 非阻塞发送数据
void SetNonBlock();//设置套接字非阻塞
void SetAddrReuse();//设置地址信息和端口号复用
/*-- - 整合功能-- -*/
bool CreateServer();//创建一个服务器连接
bool CreateClient();//创建一个客户端连接
};
模块的实现
这个模块是把服务端的Socket和客户端的Socket整合到一起了
基础模块
class Socket
{
private:
int _sockfd;
public:
Socket() // 接收监听套接字的构造函数
: _sockfd(-1)
{
}
Socket(int sockfd) // 接收客户端连接后的通信套接字
: _sockfd(sockfd)
{
}
~Socket() { Close(); }
/*-- - 基础功能-- -*/
bool Create() // 创建套接字
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0)
{
ERR_LOG("Create failed");
return false;
}
std::cout << "sockfd:" << _sockfd << std::endl;
return true;
}
bool Bind(const string &ip, uint16_t port) // 绑定地址信息
{
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
socklen_t len = sizeof addr;
int n = bind(_sockfd, (struct sockaddr *)&addr, len);
if (n < 0)
{
ERR_LOG("Bind failed");
return false;
}
std::cout << "Bind:" << n << std::endl;
return true;
}
bool Listen(int backlog = MAX_LISTEN) // 服务端开始监听
{
int n = listen(_sockfd, backlog);
if (n < 0)
{
ERR_LOG("Listen failed");
return false;
}
std::cout << "Listen:" << n << std::endl;
return true;
}
bool Connect(const string &ip, uint16_t port) // 向服务端发起连接
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
socklen_t len = sizeof addr;
int n = connect(_sockfd, (struct sockaddr *)&addr, len);
if (n < 0)
{
ERR_LOG("Connect failed");
return false;
}
std::cout << "Connect:" << n << std::endl;
return true;
}
int Accept() // 获取客户端连接
{
int connfd = accept(_sockfd, nullptr, nullptr);
std::cout << "Accept:" << connfd << std::endl;
if (connfd < 0)
{
ERR_LOG("Accept failed");
return -1;
}
return connfd;
}
ssize_t Send(const void *buf, size_t len, int flag = 0) // 将数据从用户态缓冲区发送到内核缓冲区
{
int n = send(_sockfd, buf, len, flag);
if (n < 0)
{
if (errno == EAGAIN || errno == EINTR)
{
return 0;
ERR_LOG("send failed");
return -1;
}
}
return n;
}
ssize_t Recv(void *buf, size_t len, int flag = 0) // 接收数据
{
int n = recv(_sockfd, buf, len, flag);
if (n < 0)
{
if (errno == EAGAIN || errno == EINTR)
{
return 0;
ERR_LOG("recv failed");
return -1;
}
}
return n;
}
void Close() // 关闭套接字
{
close(_sockfd);
}
ssize_t SendNonBlock(void *buf, size_t len) // 非阻塞发送数据
{
return Recv(buf, len, MSG_DONTWAIT);
}
};
特殊模块
为什么需要把套接字设置成非阻塞属性呢?
设置非阻塞其实就两个步骤,首先获取描述符当前属性,然后再在获取到的属性上加上我们的非阻塞属性,再将其设置进描述符中。 这里需要用到 fcntl() 接口
int fcntl(int fd, int cmd, ... /* arg */);
man手册中说明了,获取和设置O_CLOEXEC 也就是不可被拷贝,是使用 F_GETFD和F_SETFD,而其他属性的获取和设置则需要使用F_GETFL 和 F_SETFL。
// 设置套接字非阻塞属性
void SetNonBlock()
{
// int fcntl(int fd, int cmd, ...)
int flag = fcntl(_sockfd, F_GETFL, 0);
fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}
地址信息和端口号复用需要用到的 setsockopt() 接口
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
第一个参数就是要设置的文件描述符或者说套接字,第二个参数就是要设置的层级,第三个参数表示要进行什么操作,第四个参数表示要设置的值,1 表示激活,0表示取消,第四个参数表示第三个参数的大小。
void SetAddrReuse() // 设置地址信息和端口号复用
{
int val = 1;
int ret = setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &val, sizeof val);
// 设置 SO_REUSEADDR 可以绑定处于 TIME_WAIT 状态的端口
// 设置 SO_REUSEPORT 可以让一个端口被多个 socket 绑定,可以用于实现负载均衡
}
集成模块
然后就是设计两个集成端口,首先创建服务器套接字,他需要创建一个套接字,绑定端口和IP,设置非阻塞,还要设置地址复用,以及开始监听。
bool CreateServer(uint16_t port, const string &ip = "0.0.0.0", bool nonblock = false) // 创建一个服务器连接
{
// 1.创建套接字 2.绑定地址 3.开始监听 4.打开地址重用 5.设置非阻塞
if (Create() == false)
return false;
if (Bind(ip, port) == false)
return false;
if (Listen() == false)
return false;
SetAddrReuse();
if (nonblock)
SetNonBlock();
return true;
}
而建立一个客户端套接字,他也需要创建套接字,但是它不需要我们显式绑定端口和IP,然后就是调用Conect进行连接服务器。注意客户端套接字不要connect之前设置非阻塞,因为如果设置了非阻塞,那么我们就无法判断connect是否连接成功。
bool CreateClient(uint16_t port, const string &ip) // 创建一个客户端连接
{
// 1.创建套接字 2.连接客户端
if (Create() == false)
return false;
if (Connect(ip, port) == false)
return false;
return true;
}
主函数
主函数实现
server.cc
#include <iostream>
#include "Socket.hpp"
#define PORT 8080
int main()
{
// 创建服务端套接字
Socket serverSocket;
// 创建并初始化服务端
if (!serverSocket.CreateServer(PORT, "0.0.0.0", true))
{
std::cerr << "Server initialization failed!" << std::endl;
return -1;
}
std::cout << "Server is listening on port " << PORT << "..." << std::endl;
// 接受客户端连接
int clientSocket = -1;
while (1)
{
sleep(1);
clientSocket = serverSocket.Accept();
if (clientSocket < 0)
{
std::cerr << "Failed to accept client connection!" << std::endl;
}
else
{
break;
}
}
std::cout << "Client connected!" << std::endl;
Socket conSocket(clientSocket);
int RecvCount = 5;
// 接收数据
while (RecvCount)
{
char buffer[1024] = {0};
ssize_t bytesReceived = conSocket.Recv(buffer, sizeof(buffer));
if (bytesReceived > 0)
{
std::cout << "Received from client: " << buffer << std::endl;
RecvCount--;
}
else
{
std::cerr << "Failed to receive data from client!" << std::endl;
}
sleep(1);
}
// 发送数据到客户端
int SendCount = 5;
while (SendCount)
{
const char *response = "Hello from server!";
ssize_t bytesSent = conSocket.Send(response, strlen(response));
if (bytesSent > 0)
{
std::cout << "Sent to client: " << response << std::endl;
SendCount--;
}
else
{
std::cerr << "Failed to send data to client!" << std::endl;
}
sleep(1);
}
// 关闭连接
serverSocket.Close();
std::cout << "Server closed!" << std::endl;
return 0;
}
client.cc
#include <iostream>
#include "Socket.hpp"
#define PORT 8080
int main()
{
// 创建客户端套接字
Socket clientSocket;
// 创建并初始化客户端
if (!clientSocket.CreateClient(PORT, "127.0.0.1"))
{
std::cerr << "Client initialization failed!" << std::endl;
return -1;
}
std::cout << "Client connected to server!" << std::endl;
// 发送数据到服务端
int SendCount = 5;
while (SendCount)
{
const char *message = "Hello from client!";
ssize_t bytesSent = clientSocket.Send(message, strlen(message));
if (bytesSent > 0)
{
std::cout << "Sent to server: " << message << std::endl;
SendCount--;
}
else
{
std::cerr << "Failed to send data to server!" << std::endl;
}
sleep(1);
}
// 接收数据从服务端
int RecvCount = 5;
while (RecvCount)
{
char buffer[1024] = {0};
ssize_t bytesReceived = clientSocket.Recv(buffer, sizeof(buffer));
if (bytesReceived > 0)
{
std::cout << "Received from server: " << buffer << std::endl;
RecvCount--;
}
else
{
std::cerr << "Failed to receive data from server!" << std::endl;
}
sleep(1);
}
// 关闭连接
clientSocket.Close();
std::cout << "Client closed!" << std::endl;
return 0;
}
主函数测试
客户端往服务端发送数据
Client.cc
const char *message = "Hello from client!";
ssize_t bytesSent = clientSocket.Send(message, strlen(message));
这两段代码的过程如下:
Server.CC
char buffer[1024] = {0};
ssize_t bytesReceived = conSocket.Recv(buffer, sizeof(buffer));
这两段代码的过程如下:
服务端与客户端双向发送/接收,以及关闭
疑惑点
关于套接字的基础功能,为啥已经建立连接了,后面又获取连接了?
因为你建立的连接是客户端->服务端的,而获取新连接,是指的服务端获取到了客户端的连接。
socket函数的用法
int socket(int domain, int type, int protocol);
这里为啥要用到memset?
bind函数的用法
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
accept的用法
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
send的用法
send read write接口的解释
ssize_t send(const void* buf, size_t len, int flag = 0) 这里的buf指的是内核发送缓冲区还是用户态的输出缓冲区?
send的sockfd参数是干嘛的
是不是意思就是说,A与B进行通信的话 会有个sockfd1 C与D进行通信的话,会有个sockfd2?
不是的
但是每对通信的客户端和服务端都需要独立的套接字进行数据传输
send发送数据
send发送数据指的是从用户态缓冲区发送到内核态缓冲区,并不是从客户端发送到服务端了
这里send函数为啥不传_sockfd
为什么会有两个构造函数
send()是把数据从用户输出缓冲区发送到内核的输出缓冲区中 这个内核的输出缓冲区是指的自己的内核缓冲区 还是对端的内核缓冲区?
为什么Accept的返回值为-1
因为此时客户端还没连接,但已经调用了获取客户端的接口。所以会返回-1
CTRL + Z后,再次启动bind失败
客户端发送了数据 但是服务端收不到数据