目录
1.认识TCP接口
2.Echo Server
2.1添加的日志系统(代码)
2.2解析网络地址
2.3 服务端逻辑 (代码)
2.4客户端逻辑(代码)
2.5代码测试
1.认识TCP接口
下面介绍程序中用到的 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 参数是这样初始化的:
1. 将整个结构体清零;
2. 设置地址类型为 AF_INET;
3. 网络地址为 INADDR_ANY, 这个宏表示本地的任意 IP 地址,因为服务器可能有
多个网卡,每个网卡也可能绑定多个 IP 地址, 这样设置可以在所有的 IP 地址上监听,
直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址;
4. 端口号为 SERV_PORT, 我们定义为 9999;listen():
• listen()声明 sockfd 处于监听状态, 并且最多允许有 backlog 个客户端处于连接
等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是 5)。
• listen()成功返回 0,失败返回-1;
accept():
• 三次握手完成后, 服务器调用 accept()接受连接;
• 如果服务器调用 accept()时还没有客户端的连接请求,就阻塞等待直到有客户端
连接上来;
• addr 是一个传出参数,accept()返回时传出客户端的地址和端口号;
• 如果给 addr 参数传 NULL,表示不关心客户端的地址;
• addrlen 参数是一个传入传出参数(value-result argument), 传入的是调用者提
供的, 缓冲区 addr 的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实
际长度(有可能没有占满调用者提供的缓冲区);• 返回值:
如果
accept()
成功,它将返回一个新的套接字描述符,这个描述符用于与客户端的通信。原始的sockfd
套接字描述符则继续留在监听状态,准备接受新的连接请求。如果
accept()
失败,它将返回-1
,并设置全局变量errno
以指示错误的类型。我们的服务器程序结构是这样的:
connect
• 客户端需要调用 connect()连接服务器;
• connect 和 bind 的参数形式一致, 区别在于 bind 的参数是自己的地址, 而
connect 的参数是对方的地址;
• connect()成功返回 0,出错返回-1;
2.Echo Server
2.1添加的日志系统(代码)
LockGuard.hpp
#pragma once #include <pthread.h> class LockGuard { public: LockGuard(pthread_mutex_t *mutex):_mutex(mutex) { pthread_mutex_lock(_mutex); } ~LockGuard() { pthread_mutex_unlock(_mutex); } private: pthread_mutex_t *_mutex; };
Log.hpp
#pragma once #include <iostream> #include <sys/types.h> #include <unistd.h> #include <ctime> #include <cstdarg> #include <fstream> #include <cstring> #include <pthread.h> #include "LockGuard.hpp" namespace log_ns { enum { DEBUG = 1, INFO, WARNING, ERROR, FATAL }; std::string LevelToString(int level) { switch (level) { case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARNING: return "WARNING"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return "UNKNOWN"; } } std::string GetCurrTime() { time_t now = time(nullptr); struct tm *curr_time = localtime(&now); char buffer[128]; snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d", curr_time->tm_year + 1900, curr_time->tm_mon + 1, curr_time->tm_mday, curr_time->tm_hour, curr_time->tm_min, curr_time->tm_sec); return buffer; } class logmessage { public: std::string _level; pid_t _id; std::string _filename; int _filenumber; std::string _curr_time; std::string _message_info; }; #define SCREEN_TYPE 1 #define FILE_TYPE 2 const std::string glogfile = "./log.txt"; pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; // log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , ); class Log { public: Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE) { } void Enable(int type) { _type = type; } void FlushLogToScreen(const logmessage &lg) { printf("[%s][%d][%s][%d][%s] %s", lg._level.c_str(), lg._id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str()); } void FlushLogToFile(const logmessage &lg) { std::ofstream out(_logfile, std::ios::app); if (!out.is_open()) return; char logtxt[2048]; snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s", lg._level.c_str(), lg._id, lg._filename.c_str(), lg._filenumber, lg._curr_time.c_str(), lg._message_info.c_str()); out.write(logtxt, strlen(logtxt)); out.close(); } void FlushLog(const logmessage &lg) { // 加过滤逻辑 --- TODO LockGuard lockguard(&glock); switch (_type) { case SCREEN_TYPE: FlushLogToScreen(lg); break; case FILE_TYPE: FlushLogToFile(lg); break; } } void logMessage(std::string filename, int filenumber, int level, const char *format, ...) { logmessage lg; lg._level = LevelToString(level); lg._id = getpid(); lg._filename = filename; lg._filenumber = filenumber; lg._curr_time = GetCurrTime(); va_list ap; va_start(ap, format); char log_info[1024]; vsnprintf(log_info, sizeof(log_info), format, ap); va_end(ap); lg._message_info = log_info; // 打印出来日志 FlushLog(lg); } ~Log() { } private: int _type; std::string _logfile; }; Log lg; #define LOG(Level, Format, ...) \ do \ { \ lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \ } while (0) #define EnableScreen() \ do \ { \ lg.Enable(SCREEN_TYPE); \ } while (0) #define EnableFILE() \ do \ { \ lg.Enable(FILE_TYPE); \ } while (0) };
2.2解析网络地址
此功能实现较为简单,请看注释:
#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> //网络地址 class InetAddr { private: void ToHost(const struct sockaddr_in &addr)//网络序列转主机序列 { _port = ntohs(addr.sin_port);//这个端口号是随机bind的 _ip = inet_ntoa(addr.sin_addr);//四字节地址转字符串 } public: InetAddr(const struct sockaddr_in &addr):_addr(addr) { ToHost(addr); } std::string Ip() { return _ip; } uint16_t Port() { return _port; } ~InetAddr() { } private: std::string _ip; uint16_t _port; struct sockaddr_in _addr;//保存一下网络序列 };
2.3 服务端逻辑 (代码)
TcpServer.hpp代码主体逻辑:
构造函数 (
TcpServer(uint16_t port = gport)
): 初始化服务器对象时,可以指定一个端口号(默认为8888)。它设置了服务器的监听端口、监听套接字(初始化为-1,实际值在InitServer
中设置)和服务器是否正在运行的标志。初始化服务器 (
InitServer()
): 这个方法负责设置服务器的网络环境。它首先创建一个socket,然后将其绑定到服务器的地址和端口上,并监听连接请求。如果在这个过程中发生任何错误(如socket创建失败、绑定失败或监听失败),则会记录错误日志并退出程序。启动服务器循环 (
Loop()
): 这个方法是服务器的主循环,它首先设置_isrunning
为true
,然后进入一个无限循环,不断接受客户端的连接请求。对于每个新的连接请求,它都会调用accept
函数来获取一个新的socket(sockfd
),该socket用于与客户端进行通信。然后,它调用Service
函数来处理这个连接。如果_isrunning
被设置为false
(尽管在这个示例中没有显示直接设置它的代码,但在实际应用中可能需要某种方式来优雅地关闭服务器),则循环会终止。处理连接 (
Service(int sockfd, InetAddr addr)
): 这个方法负责处理与客户端的通信。它进入一个无限循环,不断从客户端socket读取数据,并将读取到的数据(前面添加了"[server echo] #"前缀)发送回客户端。如果读取到0字节(表示客户端关闭了连接),或者发生读取错误,循环会终止,并且会关闭与客户端的socket连接。#pragma once #include <iostream> #include <cstring> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <pthread.h> #include "Log.hpp" #include "InetAddr.hpp" using namespace log_ns; enum { SOCKET_ERROR = 1, BIND_ERROR, LISTEN_ERR }; const static int gport = 8888; const static int gsock = -1; const static int gblcklog = 8;//套接字监听队列的最大长度 class TcpServer { public: TcpServer(uint16_t port = gport) : _port(port), _listensockfd(gsock), _isrunning(false) { } void InitServer()//初始化 { // 1. 创建socket //SOCK_STREAM表示流式套接字(TCP典型),AF_INET代表IPv4地址族 _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (_listensockfd < 0) { LOG(FATAL, "socket create error\n"); exit(SOCKET_ERROR); } //创建socket成功 LOG(INFO, "socket create success, sockfd: %d\n", _listensockfd); // 3 // 2. bind struct sockaddr_in local;//用于表示Internet地址,特别是IPv4地址和端口号。 memset(&local, 0, sizeof(local));//先清空再使用 local.sin_family = AF_INET;//表示你的套接字将使用IPv4协议。 local.sin_port = htons(_port);//端口号,htons主机序列转为网络序列 // local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时 local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定 // 2. bind sockfd 和 Socket addr if (::bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)) < 0) { LOG(FATAL, "bind error\n"); exit(BIND_ERROR); } LOG(INFO, "bind success\n"); // 3. 因为tcp是面向连接的,tcp需要未来不断地能够做到获取连接 //将套接字设置为监听进入连接的状态,准备接受客户端的连接请求 if (::listen(_listensockfd, gblcklog) < 0) { LOG(FATAL, "listen error\n"); exit(LISTEN_ERR); } LOG(INFO, "listen success\n"); } void Loop()//启动 { _isrunning = true; while (_isrunning) { struct sockaddr_in client; socklen_t len = sizeof(client); // 4. 获取新连接 //返回的为IO套接字,该过程有用户连接才通过_listensockfd获取新链接,否则阻塞等待 int sockfd = ::accept(_listensockfd, (struct sockaddr *)&client, &len); if (sockfd < 0) { LOG(WARNING, "accept error\n"); continue;//再继续accept } InetAddr addr(client); //来了一个新链接 LOG(INFO, "get a new link, client info : %s, sockfd is : %d\n", addr.AddrStr().c_str(), sockfd); Service(sockfd, addr);//提供服务 } _isrunning = false; } void Service(int sockfd, InetAddr addr) { // 长服务(客户不停止,服务器也不停止) while (true) { char inbuffer[1024]; // 当做字符串 //像读文件一样从sockfd中读(接收消息) ssize_t n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1); if (n > 0) { inbuffer[n] = 0; LOG(INFO, "get message from client %s, message: %s\n", addr.AddrStr().c_str(), inbuffer); std::string echo_string = "[server echo] #"; echo_string += inbuffer; //像写文件一样向sockfd中写(发送消息) write(sockfd, echo_string.c_str(), echo_string.size()); } else if (n == 0)//表示读到了文件结尾,表示客户端结束 { LOG(INFO, "client %s quit\n", addr.AddrStr().c_str()); break; } else {//读出错了 LOG(ERROR, "read error: %s\n", addr.AddrStr().c_str()); break; } } ::close(sockfd);//关文件描述符 } ~TcpServer() {} private: uint16_t _port; int _listensockfd;//监听套接字 bool _isrunning; };
TcpServerMain.cc
#include "TcpServer.hpp" #include <memory> // ./tcpserver 8888 int main(int argc, char *argv[]) { if(argc != 2) { std::cerr << "Usage: " << argv[0] << " local-port" << std::endl; exit(0); } uint16_t port = std::stoi(argv[1]); //port参数被传递给TcpServer的构造函数, //以便服务器知道在哪个端口上监听连接请求。 std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port); tsvr->InitServer(); tsvr->Loop(); return 0; }
2.4客户端逻辑(代码)
TcpClientMain.cc代码主体逻辑:
参数检查:首先,程序检查命令行参数的数量是否正确。它需要两个额外的参数:服务器的IP地址和端口号。如果参数数量不正确,程序会打印使用说明并退出。
创建Socket:通过调用
socket()
函数创建一个TCP socket(即使用SOCK_STREAM
作为socket类型)。如果socket创建失败,程序会打印错误信息并退出。设置服务器地址信息:程序接着使用
sockaddr_in
结构体来存储服务器的地址信息。它首先清零整个结构体,然后设置地址族为AF_INET
(IPv4),使用htons()
函数将端口号从主机字节序转换为网络字节序,并使用inet_pton()
函数将服务器IP地址的字符串形式转换为网络字节序的二进制形式。连接到服务器:通过调用
connect()
函数尝试连接到服务器。这个函数需要socket文件描述符、指向服务器地址的指针以及地址的长度作为参数。如果连接失败,程序会打印错误信息并退出。与服务器通信:一旦连接成功,程序进入一个无限循环,等待用户从标准输入(stdin)输入消息。对于每次输入的消息,程序使用
write()
函数将其发送到服务器。然后,程序使用read()
函数从socket读取服务器发送回来的响应。如果读取到的字节数大于0,程序会将接收到的数据作为字符串打印到标准输出(stdout)。如果读取到的字节数为0或发生错误(例如连接被关闭),则跳出循环。关闭Socket:在退出循环后,程序使用
close()
函数关闭socket文件描述符,以释放与socket相关的资源。注意,
read()
函数没有处理EAGAIN
或EWOULDBLOCK
错误,这些错误可能发生在非阻塞socket上。然而,在这个例子中,socket是默认创建的,因此它是阻塞的,所以这些错误不太可能发生。如果希望处理非阻塞socket或超时,则需要在socket()
调用后设置socket为非阻塞模式,并在read()
调用时处理可能的错误情况。#include <iostream> #include <cstring> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> // ./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(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); // 1. 创建socket int sockfd = ::socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { std::cerr << "create socket error" << std::endl; exit(1); } //服务器端口号是固定的,但是客户端不是,因为客服是随机的 // client的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢? // client 需要 bind它自己的IP和端口, 但是client 不需要 “显示指明” bind它自己的IP和端口, // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口, //避免端口冲突 //填充服务器的相关信息 struct sockaddr_in server;//服务器的套接字信息 memset(&server, 0, sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(serverport);//要把主机序列转为网络序列 // inet_addr把字符串形式的ip转为4字节(进程序列转网络序列) ::inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); //客户端需要调用 connect()连接服务器; int n = ::connect(sockfd, (struct sockaddr *)&server, sizeof(server)); if (n < 0) {//连接失败 std::cerr << "connect socket error" << std::endl; exit(2); } while(true)//连接成功后,客户端与服务端进行通讯 { std::string message; std::cout << "Enter #"; std::getline(std::cin, message); //发消息 write(sockfd, message.c_str(), message.size()); char echo_buffer[1024]; //读消息 n = read(sockfd, echo_buffer, sizeof(echo_buffer)); if(n > 0) { echo_buffer[n] = 0; std::cout << echo_buffer << std::endl; } else { break; } } ::close(sockfd); return 0; }