和连接中的UDP类似,实现一个简单的英译汉的功能https://blog.csdn.net/Damn_Yang/article/details/88382298
封装TCP socket
tcp_socket.hpp
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <cassert>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
#define CHECK_RET(exp)
if (!(exp))
{
return false;
}
class TcpSocket {
public:
TcpSocket()
: fd_(-1)
{ }
TcpSocket(int fd)
: fd_(fd)
{ }
bool Socket()
{
fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
printf("open fd = %d\n", fd_);
return true;
}
bool Close() const
{
close(fd_);
printf("close fd = %d\n", fd_);
return true;
}
bool Bind(const std::string& ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return false;
}
return true;
}
bool Listen(int num) const
{
int ret = listen(fd_, num);
if (ret < 0)
{
perror("listen");
return false;
}
return true;
}
bool Accept(TcpSocket* peer, std::string* ip = NULL, uint16_t* port = NULL) const
{
sockaddr_in peer_addr;
socklen_t len = sizeof(peer_addr);
int new_sock = accept(fd_, (sockaddr*)&peer_addr, &len);
if (new_sock < 0)
{
perror("accept");
return false;
}
printf("accept fd = %d\n", new_sock);
peer->fd_ = new_sock;
if (ip != NULL)
{
*ip = inet_ntoa(peer_addr.sin_addr);
}
if (port != NULL)
{
*port = ntohs(peer_addr.sin_port);
}
return true;
}
bool Recv(std::string* buf) const
{
buf->clear();
char tmp[1024 * 10] = { 0 };
// [注意!] 这里的读并不算很严谨, 因为一次 recv 并不能保证把所有的数据都全部读完
// 参考 man 手册 MSG_WAITALL 节.
ssize_t read_size = recv(fd_, tmp, sizeof(tmp), 0);
if (read_size < 0)
{
perror("recv");
return false;
}
if (read_size == 0)
{
return false;
}
buf->assign(tmp, read_size);
return true;
}
bool Send(const std::string& buf) const
{
ssize_t write_size = send(fd_, buf.data(), buf.size(), 0);
if (write_size < 0)
{
perror("send");
return false;
}
return true;
}
bool Connect(const std::string& ip, uint16_t port) const
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = connect(fd_, (sockaddr*)&addr, sizeof(addr));
if (ret < 0)
{
perror("connect");
return false;
}
return true;
}
int GetFd() const
{
return fd_;
}
private:
int fd_;
};
TCP通用服务器
tcp_server.hpp
#pragma once
#include <functional>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string& req, std::string* resp)> Handler;
class TcpServer
{
public:
TcpServer(const std::string& ip, uint16_t port)
: ip_(ip)
, port_(port)
{ }
bool Start(Handler handler)
{
// 1. 创建 socket;
CHECK_RET(listen_sock_.Socket());
// 2. 绑定端口号
CHECK_RET(listen_sock_.Bind(ip_, port_));
// 3. 进行监听
CHECK_RET(listen_sock_.Listen(5));
// 4. 进入事件循环
for (;;)
{
// 5. 进行 accept
TcpSocket new_sock;
std::string ip;
uint16_t port = 0;
if (!listen_sock_.Accept(&new_sock, &ip, &port))
{
continue;
}
printf("[client %s:%d] connect!\n", ip.c_str(), port);
// 6. 进行循环读写
for (;;)
{
std::string req;
// 7. 读取请求. 读取失败则结束循环
bool ret = new_sock.Recv(&req);
if (!ret)
{
printf("[client %s:%d] disconnect!\n", ip.c_str(), port);
// [注意!] 需要关闭 socket
new_sock.Close();
break;
}
// 8. 计算响应
std::string resp;
handler(req, &resp);
// 9. 写回响应
new_sock.Send(resp);
printf("[%s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());
}
}
return true;
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint64_t port_;
};
英译汉服务器
dict_server.cc
#include <unordered_map>
#include "tcp_server.hpp"
std::unordered_map<std::string, std::string> g_dict;
void Translate(const std::string& req, std::string* resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end())
{
*resp = "未找到";
return;
}
*resp = it->second;
return;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("Usage ./dict_server [ip] [port]\n");
return 1;
}
// 1. 初始化词典
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
// 2. 启动服务器
TcpServer server(argv[1], atoi(argv[2]));
server.Start(Translate);
return 0;
}
TCP通用客户端
tcp_client.hpp
#pragma once
#include "tcp_socket.hpp"
class TcpClient {
public:
TcpClient(const std::string& ip, uint16_t port)
: ip_(ip)
, port_(port)
{
// [注意!!] 需要先创建好 socket
sock_.Socket();
}
~TcpClient()
{
sock_.Close();
}
bool Connect()
{
return sock_.Connect(ip_, port_);
}
bool Recv(std::string* buf)
{
return sock_.Recv(buf);
}
bool Send(const std::string& buf)
{
return sock_.Send(buf);
}
private:
TcpSocket sock_;
std::string ip_;
uint16_t port_;
};
英译汉客户端
dict_client.cc
#include "tcp_client.hpp"
#include <iostream>
int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("Usage ./dict_client [ip] [port]\n");
return 1;
}
TcpClient client(argv[1], atoi(argv[2]));
bool ret = client.Connect();
if (!ret)
{
return 1;
}
for (;;)
{
std::cout << "请输入要查询的单词:" << std::endl;
std::string word;
std::cin >> word;
if (!std::cin)
{
break;
}
client.Send(word);
std::string result;
client.Recv(&result);
std::cout << result << std::endl;
}
return 0;
}
由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配
注意:
- 客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号。否则在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接
- 服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦
测试多个连接的情况
在启动一个客户端,尝试连接服务器,发现第二个客户端,不能正确的和服务器进行通信。
分析原因,是因为我们accept了一个请求之后,就在一致while循坏read,没有继续调用到accept,导致不能接受新的请求
我们当前这个TCP,只能处理一个连接,这是不科学的。
简单的TCP网络程序(多进程版本)
tcp_process_server.hpp
#pragma once
#include <functional>
#include <signal.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string& req, std::string* resp)> Handler;
// 多进程版本的 Tcp 服务器
class TcpProcessServer
{
public:
TcpProcessServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port)
{
// 需要处理子进程
signal(SIGCHLD, SIG_IGN);
}
void ProcessConnect(const TcpSocket& new_sock, const std::string& ip, uint16_t port,
Handler handler)
{
int ret = fork();
if (ret > 0)
{
// father
// 父进程不需要做额外的操作, 直接返回即可.
// 思考, 这里能否使用 wait 进行进程等待?
// 如果使用 wait , 会导致父进程不能快速再次调用到 accept, 仍然没法处理多个请求
// [注意!!] 父进程需要关闭 new_sock
new_sock.Close();
return;
}
else if (ret == 0)
{
// child
// 处理具体的连接过程. 每个连接一个子进程
for (;;)
{
std::string req;
bool ret = new_sock.Recv(&req);
if (!ret)
{
// 当前的请求处理完了, 可以退出子进程了. 注意, socket 的关闭在析构函数中就完成了
printf("[client %s:%d] disconnected!\n", ip.c_str(), port);
exit(0);
}
std::string resp;
handler(req, &resp);
new_sock.Send(resp);
printf("[client %s:%d] req: %s, resp: %s\n", ip.c_str(), port, req.c_str(), resp.c_str());
}
}
else
{
perror("fork");
}
}
bool Start(Handler handler)
{
// 1. 创建 socket;
CHECK_RET(listen_sock_.Socket());
// 2. 绑定端口号
CHECK_RET(listen_sock_.Bind(ip_, port_));
// 3. 进行监听
CHECK_RET(listen_sock_.Listen(5));
// 4. 进入事件循环
for (;;)
{
// 5. 进行 accept
TcpSocket new_sock;
std::string ip;
uint16_t port = 0;
if (!listen_sock_.Accept(&new_sock, &ip, &port))
{
continue;
}
printf("[client %s:%d] connect!\n", ip.c_str(), port);
ProcessConnect(new_sock, ip, port, handler);
}
return true;
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint64_t port_;
};
dict_server.cc稍加修改
将TcpServer类改成TcpProcessServer类即可
简单的TCP网络程序(多线程版本)
通过每个请求,创建一个线程的方式来支持多连接;
tcp_thread_server.hpp
#pragma once
#include <functional>
#include <pthread.h>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string&, std::string*)> Handler;
struct ThreadArg
{
TcpSocket new_sock;
std::string ip;
uint16_t port;
Handler handler;
};
class TcpThreadServer
{
public:
TcpThreadServer(const std::string& ip, uint16_t port)
: ip_(ip)
, port_(port)
{ }
bool Start(Handler handler)
{
// 1. 创建 socket;
CHECK_RET(listen_sock_.Socket());
// 2. 绑定端口号
CHECK_RET(listen_sock_.Bind(ip_, port_));
// 3. 进行监听
CHECK_RET(listen_sock_.Listen(5));
// 4. 进入循环
for (;;)
{
// 5. 进行 accept
ThreadArg* arg = new ThreadArg();
arg->handler = handler;
bool ret = listen_sock_.Accept(&arg->new_sock, &arg->ip, &arg->port);
if (!ret)
{
continue;
}
printf("[client %s:%d] connect\n", arg->ip.c_str(), arg->port);
// 6. 创建新的线程完成具体操作
pthread_t tid;
pthread_create(&tid, NULL, ThreadEntry, arg);
pthread_detach(tid);
}
return true;
}
// 这里的成员函数为啥非得是 static?
static void* ThreadEntry(void* arg)
{
// C++ 的四种类型转换都是什么?
ThreadArg* p = reinterpret_cast<ThreadArg*>(arg);
ProcessConnect(p);
// 一定要记得释放内存!!! 也要记得关闭文件描述符
p->new_sock.Close();
delete p;
return NULL;
}
// 处理单次连接. 这个函数也得是 static
static void ProcessConnect(ThreadArg* arg)
{
// 1. 循环进行读写
for (;;)
{
std::string req;
// 2. 读取请求
bool ret = arg->new_sock.Recv(&req);
if (!ret)
{
printf("[client %s:%d] disconnected!\n", arg->ip.c_str(), arg->port);
break;
}
std::string resp;
// 3. 根据请求计算响应
arg->handler(req, &resp);
// 4. 发送响应
arg->new_sock.Send(resp);
printf("[client %s:%d] req: %s, resp: %s\n", arg->ip.c_str(), arg->port, req.c_str(), resp.c_str());
}
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint16_t port_;
};