基于 C++ 的封装
通信 socket 的封装
此处封装 socket 的功能主要是用于通信,因此包括用于通信的文件套接字 m_fd,用于通信的函数 sendMsg 和 recvMsg;另外,在通信时,会出现 TCP 粘包问题,因此通过辅助函数 readn 和 writen 来解决该问题;
对于客户端和服务器,其中服务器的套接字有两种,用于监听和用于通信;而客户端的套接字功能只有通信,因此客户端也使用当前套接字。
由于是用于通信的 socket,因此客户端直接使用该套接字即可;因此在该类中定义了客户端连接服务器的函数
connectToHost
;
#ifndef _TCPSOCKET_H_
#define _TCPSOCKET_H_
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
using namespace std;
class Socket
{
public:
Socket();
Socket(int socket);
~Socket();
int connectHost(string ip, unsigned short port);
string recvMsg();
int sendMsg(string msg);
private:
int readn(char* msg, int size);
int writen(const char* msg, int size);
private:
int m_fd;
};
#endif // _TCPSOCKET_H_
实现:
#include "Socket.h"
Socket::Socket()
{
m_fd = socket(AF_INET, SOCK_STREAM, 0);
}
Socket::Socket(int socket)
{
m_fd = socket;
}
Socket::~Socket()
{
if (m_fd > 0) {
close(m_fd);
}
}
// 知晓 ip 和 port 后,直接连接服务器,注意大端小端的转换!!!
int Socket::connectHost(string ip, unsigned short port)
{
sockaddr_in addr;
// string.data() 返回一个指向字符串内部字符串数组的指针
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip.data(), (char*)&addr.sin_addr.s_addr);
addr.sin_port = htons(port);
int ret = connect(m_fd, (sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
cout << "connect fail..." << endl;
return -1;
}
return ret;
}
// 接收时注意到信息最开始四个字节为信息大小,这可以解决粘包问题
string Socket::recvMsg()
{
int len = 0;
// 首先接收前四个字节的信息,即后面整条信息的长度
read(m_fd, &len, 4);
len = ntohl(len);
cout << "信息大小为:" << len << endl;
char* msg = new char[len + 1];
int ret = readn(msg, len);
if (ret != len) {
cout << "recvMsg fail...\n" << endl;
return string();
}
msg[len] = '\0';
string res(msg);
delete[] msg;
return res;
}
// 在信息前加上大小,以解决粘包问题
int Socket::sendMsg(string msg)
{
int len = msg.size();
char* buf = new char[len + 4];
int bigLen = htonl(len);
memcpy(buf, &bigLen, 4);
memcpy(buf + 4, msg.data(), len);
int ret = writen(buf, len + 4);
if (ret != len) {
cout << "send fail...\n" << endl;
return -1;
}
delete[] buf;
return ret;
}
// 一次性接收大小为 size 的 msg 信息
int Socket::readn(char *msg, int size)
{
char* p = msg;
int count = size;
while (count > 0) {
int len = recv(m_fd, p, count, 0);
if (len == -1) {
cout << "recv fail...\n" << endl;
return -1;
} else if (len == 0) {
continue;
}
count -= len;
p += len;
}
return size;
}
// 一次性将大小为 size 的信息 msg 发送出去
int Socket::writen(const char *msg, int size)
{
const char* p = msg;
int count = size;
while (count > 0) {
int len = send(m_fd, p, count, 0);
if (len == 0) {
continue;
} else if (len == -1) {
cout << "send fail...\n" << endl;
return -1;
}
p += len;
count -= len;
}
return size;
}
服务端的封装
服务端主要有两个功能,监听客户端请求和和客户端通信。通信的套接字在 Socket 中已经定义了,此处仅仅定义具有监听功能的套接字。
因此成员变量只有一个,即用于监听的套接字。而成员函数主要有两个,即将服务器 ip 和 port 绑定到套接字上的函数,以及创建通信套接字的函数。因此有如下定义:
#ifndef _TCPSERVER_H_
#define _TCPSERVER_H_
#include "Socket.h"
class Server {
public:
Server();
~Server();
// 绑定时只需要指定端口即可,ip 可以自动获取
int BindAndListen(unsigned short port);
// 和客户端建立连接后需要记录客户端的 addr,并获取用于通信的 socket
Socket* acceptConn(sockaddr_in* addr);
private:
int m_fd;
};
#endif // _TCPSERVER_H_
实现:
#include "Server.h"
Server::Server()
{
m_fd = socket(AF_INET, SOCK_STREAM, 0);
}
Server::~Server()
{
if (m_fd > 0) {
close(m_fd);
}
}
int Server::BindAndListen(unsigned short port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(m_fd, (sockaddr*)&addr, sizeof(addr));
if (ret == -1) {
cout << "bind fail...\n" << endl;
return -1;
}
cout << "套接字绑定成功, ip 为" << inet_ntoa(addr.sin_addr) << endl;
ret = listen(m_fd, 128);
if (ret == -1) {
cout << "listen fail...\n" << endl;
return -1;
}
cout << "设置监听成功" << endl;
return ret;
}
Socket *Server::acceptConn(sockaddr_in *addr)
{
if (addr == nullptr) {
return nullptr;
}
socklen_t len = sizeof(addr);
int cfd = accept(m_fd, (sockaddr*)addr, &len);
if (cfd == -1) {
cout << "accept fail...\n" << endl;
return nullptr;
}
cout << "成功建立连接" << endl;
return new Socket(cfd);
}
测试-客户端
客户端仅仅一个单一的功能,即通信;因此直接使用 Socket 即可;
- 定义 Socket;
- 读取文件到指定大小的缓存 tmp 中;
- 使用 Socket 封装的函数发送;
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include "Socket.h"
using namespace std;
int main() {
Socket sk;
// 注意此处的 ip 改为自己的 ip
int ret = sk.connectHost("127.0.0.1", 9898);
if (ret == -1) {
cout << "connect fail...\n" << endl;
return -1;
}
int fd1 = open("LQ.txt", O_RDONLY);
char tmp[128];
int len = 0;
memset(tmp, 0, sizeof(tmp));
while ((len = read(fd1, tmp, sizeof(tmp))) > 0) {
sk.sendMsg(string(tmp, len));
cout << "send msg" << endl;
cout << tmp << "\n\n\n" << endl;
memset(tmp, 0, sizeof(tmp));
usleep(300);
}
sleep(10);
return 0;
}
测试-服务端
服务端用于接收客户端请求(监听);以及和客户端通信;此处要求服务端能处理多个客户端的请求,因此此处使用多线程进行处理;
-
服务端首先定义服务端的对象 Server,该对象用于监听客户端请求;
-
在设置好 bind 和 listen 后即可通过 accept 与客户端建立请求后进行通信;
-
由于需要处理多个客户端,因此此处使用一个死循环来进行 accept 以建立和客户端的通信,并通过创建线程的方式来在线程能创建一个用于通信的套接字。
-
该线程专门用于通信。因此需要创建一个函数,该函数负担了当前线程的任务,即与客户端进行通信。该函数若要和客户端进行通信,所需要的信息是客户端的信息,该信息是通过 accept 得到的,需要通过参数传递给函数;处理客户端的 addr,还需要的信息是用于通信的 socket,这也是通过 accept 获取的。由于要传递多个参数,因此将这两个进行封装,将其封装为结构体进行参数传递。
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include <pthread.h>
#include "Server.h"
using namespace std;
struct SockInfo {
Socket* sock;
sockaddr_in addr;
};
void* working(void* arg) {
SockInfo* info = static_cast<SockInfo*> (arg);
char ip[32];
printf("客户端的IP: %s, 端口: %d\n",
inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(info->addr.sin_port));
while (1) {
cout << "接收数据" << endl;
string msg = info->sock->recvMsg();
if (!msg.empty()) {
cout << msg << "\n\n\n";
} else {
break;
}
}
delete info->sock;
delete info;
return nullptr;
}
int main() {
Server ss;
int ret = ss.BindAndListen(9898);
if (ret == -1) {
cout << "bind and listen fail...\n" << endl;
return -1;
}
while (1) {
SockInfo* info = new SockInfo;
Socket *sock = ss.acceptConn(&info->addr);
info->sock = sock;
pthread_t tid;
pthread_create(&tid, nullptr, working, info);
pthread_detach(tid);
}
return 0;
}
测试结果
g++ Socket.cpp Server.cpp server.cpp -o server -lpthread
g++ client.cpp Socket.cpp -o client
先启动服务端,后启动客户端:
./server
./client
即可顺利进行测试;若需要测试多线程功能,则仿造以上的客户端再写一个即可;