目录
设计思路
类的设计
模块的实现
基础模块
特殊模块
集成模块
主函数
主函数实现
主函数测试
疑惑点
设计思路
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 8080int 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 8080int 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失败
客户端发送了数据 但是服务端收不到数据