tcp_socket
- 一、tcp_server与udp_server一样的部分
- 二、listen接口(监听)
- 三、accept接收套接字
- 1、为什么还要多一个套接字(明明已经有了个socket套接字文件了,为什么要多一个accept套接字文件?)
- 2、底层拿到新连接并根据连接进行通信
- 3、类比理解监听套接字和连接套接字的区别
- 四、服务端提供服务(向客户端回消息)
- 五、tcp_client客户端编写
- 1、框架
- 2、客户端卡退了,服务端怎么处理?(read返回值为0)
- 3、一个有趣的现象--两个一样的客户端去连接客户端?(单进程服务)
- 4、方法1:子进程关listensock,父进程关sockfd
- 5、处理waitpid问题:孙子进程处理机制或者signal忽略信号
- 6、方法2:多线程版本
- 7、方法3:线程池版本
- (1)线程池代码ThreadPool.hpp
- (2)任务代码Task.hpp
- (3)代码改进
- (4)结果
- 六、服务端翻译小程序
- 七、进化版:出现错误的细节问题
- 1、向一个已经关闭的文件描述符的文件中进行写入,读端已经关掉了,写端继续写,OS会把客户端进程杀掉
- 2、重连
- 八、在线翻译服务+重连
- 九、地址复用
- 十、守护进程介绍
- 十一、tcp的通信原理
一、tcp_server与udp_server一样的部分
#pragma once
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include "Log.hpp"
Log lg;
const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const uint16_t defaultport = 8080;
enum
{
SocketERR=2,
BINDERR=3
};
class TcpServer
{
public:
TcpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
: _socketfd(defaultfd)
, _port(port)
, _ip(ip)
{}
void InitServer()
{
_socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (_socketfd < 0)
{
lg(Fatal, "create socket err, errno: %d, errst: %s", errno, strerror(errno));
exit(SocketERR);
}
lg(Info, "create socket successful, sockfd:%d", _socketfd);
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(Fatal, "bind socket err, errno: %d, errst: %s", errno, strerror(errno));
exit(BINDERR);
}
lg(Info, "bind sucessful");
// 监听套接字 -- 因为tcp是要等待别人来连接的,所以要有个监听套接字进行监听等待别人来连接
}
void RunServer()
{
}
~TcpServer(){}
private:
int _socketfd;
uint16_t _port;
std::string _ip;
};
这里我们用inet_aton将本地序列转化成为网络序列。
二、listen接口(监听)
启动服务器,状态是listen:
三、accept接收套接字
1、为什么还要多一个套接字(明明已经有了个socket套接字文件了,为什么要多一个accept套接字文件?)
各司其职呗~~_socketfd是接客用的,面对随时来的新连接先接客到底层去,而accept的返回值才真正是服务的套接字,也就是I/O端口进行服务的,从底层拿出来进行服务!所以_socketfd只有一个,而accept返回值却有多个!(一个接客,多个服务员)
修改一下socket套接字为listen套接字:
2、底层拿到新连接并根据连接进行通信
3、类比理解监听套接字和连接套接字的区别
相当于我们去一家饭店,监听套接字是外面迎客的人,把人都迎进来,里面肯定有服务员吧,服务员就是连接套接字,服务员去服务,迎客的人去迎客。我们目前实现的是迎客连接一条龙,也就是来一群人,一个个迎客,再进来一个个服务,太慢了,所以我们的目标是实现迎客和服务两条线,来了人和迎客互不耽误,两者并发式的运行,就需要我们用多线程版本,但是会出现很多问题我们在下面一一进行讲解。
四、服务端提供服务(向客户端回消息)
那我们就写一个Server函数进行封装来将服务端进行提供服务!我们传参传accept从底层拿到的套接字和拿到的套接字的ip地址和port,我们找到ip地址用的是inet_ntop函数接口。
小问题:我上来通信的字符串和数字等难道到网络中不考虑大小端问题?我地址需要转大端,难道通信的字符串不用转吗?答案是肯定要转的,但是它网络里面自动帮忙转了。
我们用简单的Server函数中的代码为接收到消息,拼接一下再返回给服务器:
五、tcp_client客户端编写
1、框架
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string& proc)
{
std::cout << "\n\rUsages: " << proc << "serverip serverport\n" << std::endl;
}
// ./tcpclient serverip serverport
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int socketfd = socket(AF_INET, SOCK_STREAM, 0);
if (socketfd < 0)
{
std::cerr << "socket create error " << std::endl;
return 1;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
// tcp客户端要绑定,不需要显示绑定,os随机分配
// 向网络服务器请求连接,客户端发起connect的时候,进行自动随机绑定
int n = connect(socketfd, (struct sockaddr*)&server, sizeof(server));
if (n < 0)
{
std::cerr << "connect err " << std::endl;
return 2;
}
// 连接成功
std::string message;
while (true)
{
std::cout << "Please Enter@ ";
std::getline(std::cin, message);
char inbuffer[4096];
write(socketfd, message.c_str(), message.size()); // 写进socketfd
int n = read(socketfd, inbuffer, sizeof(inbuffer)); // 从socketfd读入inbuffer
if (n <= 0)
{
std::cerr << "read err " << std::endl;
return 3;
}
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
close(socketfd);
return 0;
}
2、客户端卡退了,服务端怎么处理?(read返回值为0)
客户端卡退了,服务端怎么办?
服务端保存好消息,其余不管,也就是我们的服务器read的返回值为0,也就是从底层拿到的连接的文件描述符值为0的时候,表示客户端退出。
3、一个有趣的现象–两个一样的客户端去连接客户端?(单进程服务)
理由是单进程版,得等一个进程搞好退出后才能实现另一个进程的使用。
4、方法1:子进程关listensock,父进程关sockfd
因为子进程会有很多没必要的listensock套接字,父进程会有很多没必要的sockfd套接字,子进程是进行监听,父进程是进行连接,其套接字本质不一样,子进程负责监听,父进程负责连接,所以把这些没必要的套接字都关了。
但这种情况父进程用waitpid还是有很大问题,因为父进程得等子进程退出!所以跟单进程没什么区别了,下面我们介绍怎么解决这个问题:
5、处理waitpid问题:孙子进程处理机制或者signal忽略信号
因为父进程等待子进程是阻塞的方式,导致的使父进程要一直等待子进程退出,子进程退出需要一定的时间,并且连的子进程多了,子进程就会一直运行,等待一个运行后再等下一个运行,时间太久了,所以我们使用一下子进程创建孙子进程的方法,子进程创建完立马退出,告诉父进程我退出了,父进程就能够执行下一步操作,而孙子进程去跑服务,并且孙子进程给操作系统进行托孤,孙子进程不受爷爷进程控制,并发的去跑进程。
但上面这个方法还是有很大的问题的,因为子进程的创建代价太大了,要有进程地址空间等很多需要创建的东西,很麻烦,所以我们用下面的这种方法:
6、方法2:多线程版本
上面的做法仍然有不合理之处,就是假如说是几亿个用户连接,那岂不是要几亿个线程,所以我们用线程池版本来解决!
7、方法3:线程池版本
(1)线程池代码ThreadPool.hpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>
struct ThreadInfo
{
pthread_t tid;
std::string name;
};
static const int defalutnum = 5;
template <class T>
class ThreadPool
{
public:
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
bool IsQueueEmpty()
{
return tasks_.empty();
}
std::string GetThreadName(pthread_t tid)
{
for (const auto &ti : threads_)
{
if (ti.tid == tid)
return ti.name;
}
return "None";
}
public:
static void *HandlerTask(void *args)
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
std::string name = tp->GetThreadName(pthread_self());
while (true)
{
tp->Lock();
while (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t();
}
}
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
}
}
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
static ThreadPool<T> *GetInstance()
{
if (nullptr == tp_) // ???
{
pthread_mutex_lock(&lock_);
if (nullptr == tp_)
{
std::cout << "log: singleton create done first!" << std::endl;
tp_ = new ThreadPool<T>();
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
ThreadPool(int num = defalutnum) : threads_(num)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
ThreadPool(const ThreadPool<T> &) = delete;
const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
std::vector<ThreadInfo> threads_;
std::queue<T> tasks_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
static ThreadPool<T> *tp_;
static pthread_mutex_t lock_;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;
template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
(2)任务代码Task.hpp
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
extern Log lg;
class Task
{
public:
Task(int sockfd, const std::string &ipstr, const uint16_t &clientport)
: _sockfd(sockfd), _clientip(ipstr), _clientport(clientport)
{
}
void run()
{
char buffer[4096];
// 因为是面向字节流的,所以读网络跟读文件一样简单
// 先读到buffer
ssize_t n = read(_sockfd, buffer, sizeof(buffer)); // 从套接字信息中读取消息存到buffer中
if (n < 0)
{
lg(Warning, "read err, readip:%s, readport:%d\n", _clientip.c_str(), _clientport);
}
else if (n == 0) // 客户端退出
{
lg(Info, "%s:%d quit, server close fd:%d", _clientip.c_str(), _clientport, _sockfd);
}
else
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string echo_string = "tcpserver echo@ ";
echo_string += buffer;
// 再写入
write(_sockfd, echo_string.c_str(), echo_string.size()); // 处理完的消息写回sockfd文件
}
close(_sockfd);
}
void operator()()
{
run();
}
private:
int _sockfd;
std::string _clientip;
uint16_t _clientport;
};
(3)代码改进
void RunServer()
{
ThreadPool<Task>::GetInstance()->Start(); // 开启线程池的单例模式
// signal(SIGCHLD, SIG_IGN); // 信号忽略
lg(Info, "tcp_server is running...");
while (true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 1.获取新连接
int sockfd = accept(_listensocketfd, (struct sockaddr*)&client, &len);
if (sockfd < 0)
{
lg(Warning, "accept err, errno: %d, errst: %s", errno, strerror(errno));
continue;
}
uint16_t clientport = ntohs(client.sin_port);
char ipstr[32]; // 自定义的缓冲区
inet_ntop(AF_INET, &(client.sin_addr), ipstr, sizeof(ipstr));
// 2.根据新连接来通信
lg(Info, "get a new link, sockfd:%d, clentip:%s, clientport:%d", sockfd, ipstr, clientport);
// 3.4 线程池版本
Task t(sockfd, ipstr, clientport);
ThreadPool<Task>::GetInstance()->Push(t);
}
}
(4)结果
发送一则消息则退出线程。
六、服务端翻译小程序
Init.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
extern Log lg;
const std::string dicname = "./dict.txt";
const std::string sep = ":";
static bool Split(std::string &line, std::string* part1, std::string* part2) // line输入性参数 part1/2都是输出型参数
{
auto pos = line.find(sep);
if (pos == std::string::npos)
{
return false;
}
*part1 = line.substr(0, pos);
*part2 = line.substr(pos + 1);
return true;
}
class Init
{
public:
Init()
{
std::ifstream in(dicname);
if (!in.is_open())
{
lg(Fatal, "ifstream open %s error", dicname.c_str());
exit(1);
}
std::string line;
while (std::getline(in, line))
{
std::string part1, part2;
Split(line, &part1, &part2);
dict.insert({part1, part2});
}
in.close();
}
std::string Translation(const std::string& key)
{
auto it = dict.find(key);
if (it == dict.end()) return "Unkonw";
else return it->second;
}
private:
std::unordered_map<std::string, std::string> dict;
};
Task.hpp:
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
extern Log lg;
Init init;
class Task
{
public:
Task(int sockfd, const std::string &ipstr, const uint16_t &clientport)
: _sockfd(sockfd), _clientip(ipstr), _clientport(clientport)
{
}
void run()
{
char buffer[4096];
// 因为是面向字节流的,所以读网络跟读文件一样简单
// 先读到buffer
ssize_t n = read(_sockfd, buffer, sizeof(buffer)); // 从套接字信息中读取消息存到buffer中
if (n < 0)
{
lg(Warning, "read err, readip:%s, readport:%d\n", _clientip.c_str(), _clientport);
}
else if (n == 0) // 客户端退出
{
lg(Info, "%s:%d quit, server close fd:%d", _clientip.c_str(), _clientport, _sockfd);
}
else
{
buffer[n - 2] = 0;
std::cout << "client key# " << buffer << std::endl;
std::string echo_string = init.Translation(buffer);
// 再写入
write(_sockfd, echo_string.c_str(), echo_string.size()); // 处理完的消息写回sockfd文件
}
close(_sockfd);
}
void operator()()
{
run();
}
private:
int _sockfd;
std::string _clientip;
uint16_t _clientport;
};
七、进化版:出现错误的细节问题
1、向一个已经关闭的文件描述符的文件中进行写入,读端已经关掉了,写端继续写,OS会把客户端进程杀掉
所以我们在write的时候都需要用返回值做一层判断,防止向已经关闭掉的文件描述符中写信息。要么就加信号忽略:
2、重连
tcpclient.cc:
八、在线翻译服务+重连
我们先来这些词汇:
九、地址复用
十、守护进程介绍
守护进程
守护进程的启动bash:
带上日志文件(日志信息打印到当前路径下):
接口:默认00
十一、tcp的通信原理
tcp是全双工的:两个人吵架。