Socket 编程 TCP
TCP 网络程序
和刚才 UDP 类似. 实现一个简单的英译汉的功能
TCP socket API 详解
下面介绍程序中用到的 socket API,这些函数都在 sys/socket.h 中。
socket():
socket()打开一个网络通讯端口,如果成功的话,就像 open()一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果 socket()调用出错则返回-1,对于 IPv4, family 参数指定为 AF_INET,对于 TCP 协议,type 参数指定为 SOCK_STREAM, 表示面向流的传输协议,protocol 参数的介绍从略,指定为 0 即可。
bind():
服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用 bind 绑定一个固定的网络地址和端口号,bind()成功返回 0,失败返回-1。bind()的作用是将参数 sockfd 和 myaddr 绑定在一起, 使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号,前面讲过,struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen指定结构体的长度。
我们的程序中对 myaddr 参数是这样初始化的:
- 将整个结构体清零;
- 设置地址类型为 AF_INET;
- 网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址;
- 端口号为 SERV_PORT, 我们定义为 9999;
listen():
listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5),listen()成功返回 0,失败返回-1被监听了才可以获取链接
accept():
三次握手完成后, 服务器调用 accept()接受连接,如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;如果给 addr 参数传 NULL,表示不关心客户端的地址;addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区),accept的返回值是一个文件描述符,用来读写,获取新链接,与内核的IO文件描述符要区别开来
我们的服务器程序结构是这样的:
connect
客户端需要调用 connect()连接服务器;,connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而connect 的参数是对方的地址;connect()成功返回 0,出错返回-1;
V1 - Echo Server
Common.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <memory>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum ExitCode
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};class NoCopy
{public:
NoCopy(){}
~NoCopy(){}
NoCopy(const NoCopy&)=delete;
const NoCopy& operator= (const NoCopy &)=delete;
};#define CONV(addr) ((struct sockaddr *)&addr)
InetAddr.hpp
#pragma once
#include "Common.hpp"class InetAddr
{public:InetAddr(struct sockaddr_in &addr): _addr(addr){ //网络转主机_port = ntohs(addr.sin_port);//_ip = inet_ntoa(addr.sin_addr);char ipbuffer[64];inet_ntop(AF_INET,&_addr.sin_addr,ipbuffer,sizeof(_addr));_ip=ipbuffer;}InetAddr(const std::string &ip,uint16_t port):_ip(ip),_port(port){//主机转网络memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;inet_pton(AF_INET,_ip.c_str(),&_addr.sin_addr);_addr.sin_port=htons(_port);}InetAddr(uint16_t port):_port(port),_ip("0"){ //端口转memset(&_addr,0,sizeof(_addr));_addr.sin_family=AF_INET;_addr.sin_addr.s_addr=INADDR_ANY;_addr.sin_port = htons(_port);}uint16_t Port() { return _port; }std::string Ip() { return _ip; }const struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr *NetAddrPtr() { return CONV(_addr); }socklen_t NetAddrLen() { return sizeof(_addr); }bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr() {}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
Log.hpp
#ifndef __LOG_HPP__
#define __LOG_HPP__#include <iostream>
#include <string>
#include "Mutex.hpp"
#include <filesystem>
#include <fstream>
#include <memory>
#include <unistd.h>
#include <sstream>
#include<ctime>namespace LogModule
{const std::string sep = "\r\n";using namespace MutexModule ;// 2.刷新策略class LogStrategy{public:~LogStrategy() = default;virtual void SyncLog(const std::string &message) = 0;};// 显示器刷新日志的策略class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy() {}~ConsoleLogStrategy() {}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::cout << message << sep;}private:Mutex _mutex;};// 缺省文件路径以及文件本身const std::string defaultpath = "./log";const std::string defaultfile = "my.log";// 文件刷新日志的策略class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile): _path(path), _file(file){LockGuard lockguard(_mutex);if (std::filesystem::exists(_path)) // 判断路径是否存在{return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}void SyncLog(const std::string &message) override{LockGuard lockguard(_mutex);std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;std::ofstream out(filename, std::ios::app); // 追加写入if (!out.is_open()){return;}out << message << sep;out.close();}~FileLogStrategy() {}private:Mutex _mutex;std::string _path; // 日志文件的路径std::string _file; // 要打印的日志文件};// 形成日志等级enum class Loglevel{DEBUG,INIF,WARNING,ERROR,FATAL};std::string Level2Str(Loglevel level){switch (level){case Loglevel::DEBUG:return "DEBUG";case Loglevel::INIF:return "INIF";case Loglevel::WARNING:return "WARNING";case Loglevel::ERROR:return "ERROR";case Loglevel::FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetTimeStamp(){time_t cuur =time(nullptr);struct tm curr_tm;localtime_r(&cuur,&curr_tm);char buffer[128];snprintf(buffer,sizeof(buffer),"%4d-%02d-%02d %02d:%02d:%02d",curr_tm.tm_year+1900,curr_tm.tm_mon+1,curr_tm.tm_mday,curr_tm.tm_hour,curr_tm.tm_min,curr_tm.tm_sec);return buffer;}class Logger{public:Logger(){EnableConsoleLogStrategy();}// 选择某种策略// 1.文件void EnableFileLogStrategy(){_ffush_strategy = std::make_unique<FileLogStrategy>();}// 显示器void EnableConsoleLogStrategy(){_ffush_strategy = std::make_unique<ConsoleLogStrategy>();}// 表示的是未来的一条日志class LogMessage{public:LogMessage(Loglevel &level, std::string &src_name, int line_number, Logger &logger): _curr_time(GetTimeStamp()), _level(level), _pid(getpid()), _src_name(src_name), _line_number(line_number), _logger(logger){// 合并左半部分std::stringstream ss;ss << "[" << _curr_time << "] "<< "[" << Level2Str(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _line_number << "] "<< "- ";_loginfo = ss.str();}template <typename T>LogMessage &operator<<(const T &info){// 右半部分,可变std::stringstream ss;ss << info;_loginfo += ss.str();return *this;}~LogMessage(){if (_logger._ffush_strategy){_logger._ffush_strategy->SyncLog(_loginfo);}}private:std::string _curr_time; // 日志时间Loglevel _level; // 日志状态pid_t _pid; // 进程pidstd::string _src_name; // 文件名称int _line_number; // 对应的行号std::string _loginfo; // 合并之后的一条完整信息Logger &_logger;};LogMessage operator()(Loglevel level, std::string src_name, int line_number){return LogMessage(level, src_name, line_number, *this);}~Logger() {}private:std::unique_ptr<LogStrategy> _ffush_strategy;};//全局日志对象Logger logger;//使用宏,简化用户操作,获取文件名和行号// __FILE__ 一个宏,替换完成后目标文件的文件名// __LINE__ 一个宏,替换完成后目标文件对应的行号#define LOG(level) logger(level,__FILE__,__LINE__) #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()}#endif
Makefile
.PHONY:all
all:tcpclient tcpservertcpclient:TcpClient.ccg++ -o $@ $^ -std=c++17 -lpthread
tcpserver:TcpServer.ccg++ -o $@ $^ -std=c++17 -lpthread.PHONY:clean
clean:rm -f tcpclient tcpserver
Mutex.hpp
#pragma once
#include <pthread.h>
#include <iostream>
namespace MutexModule
{ class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}~Mutex(){pthread_mutex_destroy(&_mutex);}pthread_mutex_t *get(){return &_mutex;}private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
TcpServer.cpp
#include "TcpServer.hpp"void Usage(std::string proc)
{std::cerr<<"Usage:"<<proc<<" port"<<std::endl;}
// ./TcpServer port
int main(int argc ,char*argv[])
{if(argc!=2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port = std::stoi(argv[1]);Enable_Console_Log_Strategy();std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port);tsvr->Init();tsvr->Run();return 0;
}
TcpServer.hpp
#pragma once#include "Common.hpp"
#include "Log.hpp"
#include"InetAddr.hpp"using namespace LogModule;
using namespace MutexModule;const static int defaultsockfd = -1;
const static int backlog = 8;// 服务器往往是禁止拷贝的 继承一个禁止拷贝的类,每次实例化会对父类实例化一次,因为父类的拷贝构造删掉了所以实例化失败
class TcpServer : public NoCopy
{
public:TcpServer(uint16_t port, int sockfd = defaultsockfd): _port(port), _listensockfd(sockfd),_isrunning(false){}void Init(){//1.创建套接字_listensockfd = socket(AF_INET,SOCK_STREAM,0);if(_listensockfd < 0){LOG(Loglevel::FATAL)<<"创建套接字失败!";exit(SOCKET_ERR); }LOG(Loglevel::INIF)<<"创建套接字成功! sockfd: "<<_listensockfd;//2.bind端口号InetAddr local(_port);int n = bind(_listensockfd,local.NetAddrPtr(),local.NetAddrLen());if( n < 0){LOG(Loglevel::FATAL)<<"绑定失败!";exit(BIND_ERR);}LOG(Loglevel::FATAL)<<"绑定成功!_listensockfd: "<<_listensockfd;//3.监听 n = listen(_listensockfd,backlog);if(n<0){LOG(Loglevel::FATAL)<<"监听失败!";exit(LISTEN_ERR);}LOG(Loglevel::FATAL)<<"监听成功!";}void Service(int sockfd, InetAddr &peer){char buffer[1024];while(true){//1.先读取数据 n>0读取成功 n<0 读取失败 n=0对端链接关闭,读到文件结尾ssize_t n = read(sockfd,buffer,sizeof(buffer)-1);if( n > 0){buffer[n]=0;LOG(Loglevel::DEBUG)<<peer.StringAddr()<<"says: "<<buffer;//2.写回数据std::string echo_string ="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;}}}void Run(){_isrunning = true;while(_isrunning){ //1.获取链接struct sockaddr_in peer;socklen_t len =sizeof(sockaddr_in);int sockfd = accept(_listensockfd,CONV(peer),&len);if(sockfd<0){LOG(Loglevel::WARNING)<<"获取链接失败";continue;}InetAddr addr(peer) ;LOG(Loglevel::INIF)<<"获取链接成功,peer addr: "<<addr.StringAddr();//version0 Service(sockfd,addr);}_isrunning=false;}~TcpServer() {}private:uint16_t _port;int _listensockfd;bool _isrunning;
};
clent
server