【Linux网络】Socket编程TCP-实现Echo Server(下)

上篇:【Linux网络】Socket编程TCP-实现Echo Server(上)
https://blog.csdn.net/2402_82757055/article/details/154367478?spm=1001.2014.3001.5501
1.实现客户端
1.1 创建套接字
首先就是创建套接字。
// TcpClient.cc文件
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"// Usage: ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(ExitCode::USAGE_ERR);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "创建socket失败" << std::endl;exit(ExitCode::SOCKET_ERR);}return 0;
}
1.2 connect
这里和UDP的客户端一样不需要显式的bind,采用随机端口号的方式,这里也不需要listen也不需要accept,这都是服务器应该做的,客户端只需要直接向目标服务器发起建立连接的请求。
这里要用到函数connect,成功返回0,失败返回-1。

这些参数都很眼熟了,不做过多解释。
connect可能会出错,所以在这个退出码里再增加一个CONNECT_ERR。
// Common.hpp文件
enum ExitCode
{normal = 0,SOCKET_ERR,BIND_ERR,LISTEN_ERR,USAGE_ERR,CONNECT_ERR
};
// TcpClient.cc文件int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(ExitCode::USAGE_ERR);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "创建socket失败" << std::endl;exit(ExitCode::SOCKET_ERR);}// 2.直接向目标服务器发起建立连接的请求InetAddr client(server_ip, server_port); // 这里要主机转网络int n = connect(sockfd, client.NetAddrPtr(), client.NetAddrLen());if(n < 0){std::cout << "connect error" << std::endl;exit(ExitCode::CONNECT_ERR);}return 0;
}
1.3 发收消息
// TcpClient.cc文件
#include <iostream>
#include "Common.hpp"
#include "InetAddr.hpp"
#include <unistd.h>// Usage: ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;exit(ExitCode::USAGE_ERR);}std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1.创建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cout << "创建socket失败" << std::endl;exit(ExitCode::SOCKET_ERR);}// 2.直接向目标服务器发起建立连接的请求InetAddr client(server_ip, server_port); // 这里要主机转网络int n = connect(sockfd, client.NetAddrPtr(), client.NetAddrLen());if (n < 0){std::cout << "connect error" << std::endl;exit(ExitCode::CONNECT_ERR);}while (true){// 3.发消息std::cout << "Please Enter# ";std::string line;std::getline(std::cin, line);write(sockfd, line.c_str(), line.size());// 4.收消息char buffer[1024];ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}}close(sockfd);return 0;
}
然后先启动服务端,在启动客户端,启动客户端的瞬间就会显示accept success,如果客户端退出,服务端也会有相应的显示。

2.实现多进程版
2.1 创建子进程
首先就是要把子进程创建出来,用fork函数。
// TcpServer.hpp文件void Run(){if (_isrunning) // 不能重复启动return;_isrunning = true;// a.获取链接while (_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 没有链接时,accept会被阻塞int sockfd = accept(_listen_sockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr local(peer); // 这里需要网络转主机LOG(LogLevel::INFO) << "accept success " << local.StringAddr();// 执行任务// Service(sockfd, local); // 单进程版-只是为了测试// 多进程版pid_t id = fork(); // 创建子进程if (id < 0){LOG(LogLevel::FATAL) << "fork error";exit(ExitCode::FORK_ERR);}else if (id == 0) // 子进程{}else // 父进程{}}_isrunning = false;}
通过之前的学习,我们知道子进程会得到父进程打开的那些文件描述符,具体详解在:【Linux】匿名管道和进程池
子进程除了能看到sockfd,还可以看到listen_sockfd,但是在子进程里我们不想让他访问listen_sockfd,在父进程里,也不需要访问子进程的sockfd,所以这里要关掉(也可以不关,关了更严谨)。
// 多进程版
pid_t id = fork();
if (id < 0)
{LOG(LogLevel::FATAL) << "fork error";exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{close(_listen_sockfd);
}
else // 父进程
{close(sockfd);
}
子进程就直接执行之前的Service,执行完了就退出。
// 多进程版
pid_t id = fork();
if (id < 0)
{LOG(LogLevel::FATAL) << "fork error";exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{close(_listen_sockfd);Service(sockfd, local); // 执行任务exit(ExitCode::normal);
}
else // 父进程
{close(sockfd);
}
2.2 解决僵尸进程和父进程被阻塞问题
父进程应该要等待子进程,否则子进程会变成僵尸进程。
else // 父进程
{close(sockfd);waitpid(id, nullptr,0); // ?
}
但是这里直接用waitpid函数进行等待的话,子进程不退出父进程就会一直阻塞住,这不又变成了单进程了,创建子进程意义何在?
最推荐的解决方法有两个。
方法一:对子进程信号做处理。
子进程会向父进程发送SIGCHLD信号,我们在初始化的时候,让父进程对这个信号进行忽略。

忽略了之后父进程就不会等待子进程了,父进程就一直获取连接,子进程就执行任务,两者就可以并发运行。
方法二:子进程里再创子进程。
// 多进程版
pid_t id = fork();
if (id < 0)
{LOG(LogLevel::FATAL) << "fork error";exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{close(_listen_sockfd);if(fork() > 0) // 子进程又创建子进程,就是孙子进程exit(ExitCode::normal); // 子进程创建好孙子进程自己退出Service(sockfd, local); // 执行任务的是孙子进程exit(ExitCode::normal);
}
else // 父进程
{close(sockfd);waitpid(id, nullptr,0); // 回收子进程
}
- 子进程又创建了自己的子进程,也就是孙子进程,然后子进程自己退出(fork() > 0的情况),子进程退出后,父进程waitpid就会回收子进程,此时就不会出现僵尸进程的情况,waitpid出也不会被阻塞了。
- 而执行任务的是子进程fork出来的进程,也就是孙子进程。
- 对于孙子进程来说,孙子进程的父进程是子进程,子进程先退出,也就是孙子进程的父进程退出了,相当于孙子进程变成了孤儿进程,孤儿进程会被1号进程“领养”,处理完任务后系统会回收这个孙子进程。
先把server启动,再启动两个或者更多client,我这里就启动两个。

我们查看进程状态的时候,就会查到两个孤儿进程,他们父进程PID是1。
ps axj | head -1 && ps ajx | grep tcpserver

3.实现多线程版
线程相关讲解在【Linux】线程控制。
子线程可以拿到主线程sockfd之类的文件描述符,但是新的线程并没有重新弄一个文件描述符表出来,用的就是进程的,所有线程共享一个文件描述符表,所以线程不可以像进程那样关闭自己不需要的文件。
新线程执行完之后,主线程也要等待这些线程,但这里同样是阻塞式等待,解决方法就是设置线程分离。
因为pthread_create的第三个参数Routine函数的参数只能是void* ,但是放在类内会有隐含的this指针,所以Routine函数要设为static,这一点在【Linux】多线程创建及封装 中有详细讲解。
// TcpServer.hpp文件
#pragma once
#include "Common.hpp"
#include "InetAddr.hpp"
#include "MyLog.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>using namespace MyLog;
// const static int backlog = 8;
class TcpServer : public NoCope // TcpServer类public继承NoCope类
{
public:TcpServer(uint16_t port): _port(port),_listen_sockfd(-1),_isrunning(false){}void Init(){// 1.创建套接字_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd < 0){LOG(LogLevel::FATAL) << "创建socket失败";exit(ExitCode::SOCKET_ERR);}LOG(LogLevel::INFO) << "create listen socket success, sockfd: " << _listen_sockfd;// 2.bindInetAddr local(_port);int n = bind(_listen_sockfd, local.NetAddrPtr(), local.NetAddrLen());if (n < 0){LOG(LogLevel::FATAL) << "bind 失败";exit(ExitCode::BIND_ERR);}LOG(LogLevel::INFO) << "bind succes, sockfd: " << _listen_sockfd;// 3.设置listen状态n = listen(_listen_sockfd, 8);if (n < 0){LOG(LogLevel::FATAL) << "listen 失败";exit(ExitCode::LISTEN_ERR);}LOG(LogLevel::INFO) << "listen succes";}void Service(int sockfd, InetAddr &peer){char buffer[1024];while (true){// b.收消息ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;LOG(LogLevel::DEBUG) << "Read message, " << peer.StringAddr() << "# " << buffer;// c.发消息std::string echo_string = "Server echo$ ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << peer.StringAddr() << "读取异常";close(sockfd);break;}}}class ThreadData{public:ThreadData(int fd, InetAddr &p, TcpServer *s): sfd(fd), peer(p), self(s){}int sfd;InetAddr &peer; //peer这里是无参构造,要在InetAddr构造函数里加上他的无参构造TcpServer *self;};static void *Routine(void *args){pthread_detach(pthread_self()); // 设置线程分离ThreadData *td = static_cast<ThreadData *>(args);td->self->Service(td->sfd, td->peer); // 执行任务delete td;return nullptr;}void Run(){if (_isrunning) // 不能重复启动return;_isrunning = true;// a.获取链接while (_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 没有链接时,accept会被阻塞int sockfd = accept(_listen_sockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr local(peer); // 这里需要网络转主机LOG(LogLevel::INFO) << "accept success " << local.StringAddr();// 多线程版pthread_t tid;ThreadData *td = new ThreadData(sockfd, local, this);int n = pthread_create(&tid, nullptr, Routine, td);}_isrunning = false;}~TcpServer() {}private:uint16_t _port; // 端口号int _listen_sockfd;bool _isrunning;
};
同样先把server启动,再启动两个或者更多client,我这里就启动两个。

4.实现线程池版
用到的线程池是之前自己实现的:【Linux】线程池
// TcpServer.hpp文件
#pragma once
#include "Common.hpp"
#include "InetAddr.hpp"
#include "MyLog.hpp"
#include "ThreadPool.hpp"
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include <functional>using namespace MyLog;
using namespace MyThreadPool;
using task_t = std::function<void()>;
class TcpServer : public NoCope // TcpServer类public继承NoCope类
{
public:TcpServer(uint16_t port): _port(port),_listen_sockfd(-1),_isrunning(false){}void Init(){//...}void Service(int sockfd, InetAddr &peer){//...}void Run(){if (_isrunning) // 不能重复启动return;_isrunning = true;// a.获取链接while (_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(sockaddr_in);// 没有链接时,accept会被阻塞int sockfd = accept(_listen_sockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr local(peer); // 这里需要网络转主机LOG(LogLevel::INFO) << "accept success " << local.StringAddr();// 线程池版ThreadPool<task_t>::GetInstance()->Equeue([this, sockfd, &local](){this->Service(sockfd, local);});}_isrunning = false;}~TcpServer() {}private:uint16_t _port; // 端口号int _listen_sockfd;bool _isrunning;
};
本篇分享就到这里,拜拜~

