【Linux网络与网络编程】04.TCP Socket编程
一、TCP Socket编程接口
// 创建套接字
int socket(int domain, int type, int protocol);
// 参数:
// domain:域(协议家族),这里使用 AF_INET 表示进行网络编程
// type:网络通信传输的类型,这里选择 SOCK_STREAM表示是使用TCP协议的面向数据报传递信息
// protocol:这个参数目前置0即可
// 返回值:成功则返回一个文件描述符(所以创建套接字的本质就是创建了一个文件);失败则返回-1,并设置对应的错误码
// 填充网络信息并绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数:
// sockfd:套接字文件描述符
// addr:这是为了统一socket接口而定义的数据结构,其中的 sockaddr_in 是我们今天网络通信所要用到的,我们需要在绑定之前设置好它的协议家族,端口号和ip地址
// addrlen:这是将addr的大小传递给内核,方便判断是哪种addr
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码
// TCP是面向连接的,连接成功后才能进行通信,因此要求TCP要随时随地等待被连接
// 监听
int listen(int sockfd, int backlog);
// 参数:
// sockfd:被设置为监听状态的sockfd
// backlog:最大连接个数
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码
// 获取新连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 参数:
// sockfd:监听的sockfd
// addr:请求连接的addr结构
// addrlen:请求连接的addr结构的大小
// 返回值:成功返回一个用于服务的sockfd;失败则返回-1,并设置对应的错误码
// 客户端不需要bind,首次connect是被自动绑定
// 连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数:
// sockfd:客户端想要去连接的sockfd
// addr和addrlen:目标主机的addr结构
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码
二、TCP Socket编程
2.1 EchoSever
这里我们来仿照UDP的Echo写一个EchoSever来快速熟悉接口。
//TCPSever.hpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogMudule;
const static uint16_t defaultport = 8888;
class TCPSever
{
//目前是一个EchoSever
void Service(int sockfd)
{
char buff[1024];
while(true)
{
//TCP是面向字节流的,故而可以采用文件的接口进行读取和写入
int n=::read(sockfd,buff,sizeof(buff)-1);
if(n>0)
{
buff[n]=0;
std::string message="Client Say@";
message+=buff;
int m=::write(sockfd,message.c_str(),message.size());
if(m<0)
{
LOG(LogLevel::ERROR)<<"write failed";
}
}
else if(n==0)
{
//表示读到了文件末尾
LOG(LogLevel::INFO)<<"Client Quit……";
break;
}
else
{
LOG(LogLevel::ERROR)<<"read error";
break;
}
}
}
public:
TCPSever(uint16_t port = defaultport) : _addr(port)
{
// 创建套接字
int n = _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
if (n < 0)
{
LOG(LogLevel::FATAL) << "socket failed";
exit(1);
}
LOG(LogLevel::INFO) << "socket succeed";
// 绑定
n = ::bind(_listensockfd, _addr.NetAddr(), _addr.Len());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind failed";
exit(1);
}
LOG(LogLevel::INFO) << "bind succeed";
// 开始监听
n = ::listen(_listensockfd, 5);
if (n < 0)
{
LOG(LogLevel::FATAL) << "listen failed";
exit(1);
}
LOG(LogLevel::INFO) << "listen succeed";
}
void Run()
{
while(true)
{
//获取连接
struct sockaddr_in connected_addr;
socklen_t len=sizeof(connected_addr);
int sockfd=::accept(_listensockfd,(struct sockaddr*)&connected_addr,&len);
if(sockfd<0)
{
LOG(LogLevel::ERROR)<<"accept failed";
continue;
}
InetAddr peer(connected_addr);
LOG(LogLevel::INFO)<<"accept succeed connected is "<<peer.Addr() <<" sockfd is "<<sockfd;
//既然可以获取到信息了,那么下一步就是提供服务了
Service(sockfd);
//完成之后就要关闭sockfd
::close(sockfd);
}
}
~TCPSever()
{
::close(_listensockfd);
}
private:
int _listensockfd;
InetAddr _addr;
};
//
#include "TCPSever.hpp"
//TCPSever.cc
int main()
{
std::unique_ptr<TCPSever> ts_ptr=std::make_unique<TCPSever>();
ts_ptr->Run();
return 0;
}
这段客户端的代码我们就写好了,本篇博客采用telnet来充当客户端进行测试:
可以见得我们的代码逻辑是正确的,接下来我们要实现多线程和多进程的EchoSever
进程分离管理:
//……
void Run()
{
while (true)
{
// 2. 进程分离版本,子进程执行任务,父进程来进行监听
// 获取连接
struct sockaddr_in connected_addr;
socklen_t len = sizeof(connected_addr);
int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
if (sockfd < 0)
{
LOG(LogLevel::ERROR) << "accept failed";
continue;
}
InetAddr peer(connected_addr);
LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;
int id = fork();
if (id == 0)
{
// 子进程
::close(_listensockfd);
// 子进程再次创建子进程,子进程返回,避免父进程阻塞,孙子进程为孤儿进程,被OS领养
if(fork()>0) exit(0);
Service(sockfd);
exit(0);
}
else
{
// 父进程
::close(sockfd);
int rid=::waitpid(id,nullptr,0);
if(rid<0)
{
LOG(LogLevel::ERROR) << "waitpid failed";
}
}
}
}
//……
线程分离管理:
//……
// 3. 线程分离管理
struct ThreadData
{
int _sockfd;
TCPSever *_self;
};
static void *Handler(void *args)
{
pthread_detach(pthread_self());
ThreadData* data=(ThreadData*)args;
data->_self->Service(data->_sockfd);
return nullptr;
}
//……
void Run()
{
while (true)
{
// 3.线程管理版本的
// 获取连接
struct sockaddr_in connected_addr;
socklen_t len = sizeof(connected_addr);
int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
if (sockfd < 0)
{
LOG(LogLevel::ERROR) << "accept failed";
continue;
}
InetAddr peer(connected_addr);
LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;
ThreadData* data=new ThreadData;
data->_sockfd=sockfd;
data->_self=this;
pthread_t tid;
pthread_create(&tid, nullptr, Handler, data);
}
}
线程池管理:
//……
void Run()
{
while (true)
{
// 4.线程池管理版本
struct sockaddr_in connected_addr;
socklen_t len = sizeof(connected_addr);
int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
if (sockfd < 0)
{
LOG(LogLevel::ERROR) << "accept failed";
continue;
}
InetAddr peer(connected_addr);
LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;
//绑定生成任务
task_t task=std::bind(&TCPSever::Service,this,sockfd);
ThreadPool<task_t> ::GetInstance()->Enqueue(task);
}
}
//……
2.2 远程命令执行
//command.hpp
#pragma once
#include <string>
#include <cstring>
#include "Log.hpp"
using namespace LogMudule;
class Command
{
public:
std::string Excute(const std::string &command)
{
// 相当于pipe + excl
FILE *fp = popen(command.c_str(), "r");
if (fp == nullptr)
return std::string();
char buffer[1024];
std::string result;
while (fgets(buffer, sizeof(buffer), fp))
{
result += buffer;
}
pclose(fp);
return result;
}
};
//severtcp.hpp
//……
using command_t = std::function<std::string(const std::string)>;
//……
// 5.远程命令执行
void Service(int sockfd)
{
char buff[1024];
while (true)
{
// TCP是面向字节流的,故而可以采用文件的接口进行读取和写入
int n = ::recv(sockfd, buff, sizeof(buff) - 1,0);
if (n > 0)
{
//网络传输中以\r\n结尾
buff[n-2] = 0;
std::string message = _command(buff);
int m = ::send(sockfd, message.c_str(), message.size(),0);
if (m < 0)
{
LOG(LogLevel::ERROR) << "write failed";
}
}
else if (n == 0)
{
// 表示读到了文件末尾
LOG(LogLevel::INFO) << "Client Quit……";
break;
}
else
{
LOG(LogLevel::ERROR) << "read error";
break;
}
}
}
//……
private:
//……
command_t _command; //命令提示