1.TCP套接字编程
(1)和UDP的区别
TCP和UDP都是传输层的协议,这两个协议有着自己的特点,会体现在代码上。但也有很多是不变的。想要通信,都需要创建socket文件(TCP也是通过socket通信的),并且都需要创建sockaddr_in用于网络通信的本地信息保存(服务器要显式bind),包括sin_port端口号,sin_family协议家族,sin_addr.s_addr存储的IP。
不同点:
TCP的性质是面向连接、可靠传输、面向字节流。
UDP的性质是不面向连接、不可靠传输、面向数据报。
前面我们已经体会过UDP编程的效果了,UDP的无连接表现在客户端随时启动,绑定成功后就能够向服务器随时发信息,服务器也能接收。而TCP的面向连接就需要客户端先和服务器建立连接,连接成功才能通信,因此我们也能知道TCP下的服务端会随时随地等待被连接。
UDP面向数据报体现在代码中就是传输数据时使用的是recvfrom、sendto函数(传输的对象是数据报),而TCP面向字节流就可以像管道那样使用系统调用read和write来读写,后面会讲到。
后续的讲解会从UDP和TCP的不同点出发,大部分思路相同,小部分处理不同
(2)面向连接的特性
①socket参数调整
相比较于UDP,只需修改第二个参数type为SOCK_STREAM即可,其余不变,这样创建的socket就是TCP的套接字了。
②服务器的监听状态(listen)
TCP服务器需要将socket设置为监听状态,随时等待别人来连接。
其中第二个参数我们暂时了解,填入一个数字即可,后面会专门讲解。
这一步之后服务器就被设置为监听状态了。
③服务器获取新连接(accept)
服务器启动要获取新连接,需要使用函数accept
现在需要对这个fd进行解释。函数参数里面的fd是正在监听的socket的fd,就好比在餐厅外面拉客的服务员。当拉客成功后,就会返回一个fd,这个fd相当于餐馆内部的服务员,和外面拉客的服务员不一样。这个新的返回的fd就是负责真正数据通信的文件描述符了,而那个监听的fd会继续监听,在餐馆外面拉客,和餐厅里面提供服务的fd不一致也不冲突。
④客户端尝试连接(connect)
(3)总结
2.远程命令的实现
远程命令,即让远程主机返回本地输入的结果。和聊天室的逻辑几乎一致。
有的命令很危险,可以通过黑名单(哪些不能执行)和白名单(哪些命令可以执行)来实现保护
(1)服务器端和客户端思路
服务器端:
先是让socket进入监听状态,然后进入死循环不断accept,当accept成功后就由多线程执行通信,主线程继续回到循环中accept。采用多线程的方法让一个服务器同时服务多个客户端是核心思想。除此之外,多进程、多进程多线程都可以,实现上略有差异。
每个线程负责recv和send,就像文件操作那样。其中执行命令采用的是popen和pclose,后续可在代码中看到。
客户端:connect服务器,连接成功后直接利用双线程,一个负责写一个负责读即可,这和聊天室的逻辑一致。
(2)相关代码
①TcpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <stdint.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstdlib>
#include <deque>
#include <functional>
#include "myLog.hpp"
#include "myInetAddr.hpp"
#include "myCommon.hpp"
#include "myThreadPool.hpp"
#include "myCommand.hpp"using namespace std;
using namespace myLogModule; // 使用日志模块
using namespace myThreadPoolModule;
using namespace myCommandMoudle;const uint16_t default_port = 9000; // 默认端口号,无需指定IP
const int max_size = 1024; // 存储信息的最大字节数class TcpServer
{
public:struct ClientInfo{myInetAddr client_inet_addr; // 客户端的IP和端口号int client_fd; // 客户端的socket文件描述符};TcpServer(uint16_t port = default_port): _listen_fd(-1), // 服务端的socket文件描述符_addr_server(port), // 服务端的端口号,默认为default_port,用于构造myInetAddr对象,自动初始化IP和端口号_isrunning(false) // 记录当前服务端的运行状态//_addr_client_list() // 客户端的IP和端口号列表,不需要设置,需要服务器端accept连接之后才知道客户端的IP和端口号{// socket创建网络通信的文件_listen_fd = socket(AF_INET, SOCK_STREAM, 0); // 网络通信,TCP通信方式,0默认if (_listen_fd == -1) // 创建失败返回-1{LOG(FATAL) << "服务端初始化失败"; // 输出错误信息exit(SERVER_ERROR); // 直接退出程序,1表示服务端的异常退出}if (bind(_listen_fd, _addr_server.Get_Const_Sockaddr_ptr(), _addr_server.Get_Socklen()) == -1) // 绑定服务端的IP和端口号{LOG(FATAL) << "服务端绑定失败";exit(SERVER_ERROR); // 直接退出程序,1表示服务端的异常退出}listen(_listen_fd, 8); // 监听连接LOG(INFO) << "服务端初始化成功,已启动监听端口" << (int)default_port;}void start(){_isrunning = true; // 启动服务器myThreadPool<task_t>::GetInstance()->StartAddTask(); // 启动线程池while (_isrunning) // 服务器运行时一直循环{sockaddr_in client_addr; // 客户端的IP和端口号,用于myInetAddr设置socklen_t client_addr_len = sizeof(client_addr);int client_fd = -1; // 用于服务的fd,之后向这个fd里面写内容就可以实现通信LOG(INFO) << "客户端现在可以连接服务器,正在等待...";// 等待客户端连接while ((client_fd = accept(_listen_fd, (sockaddr *)&client_addr, &client_addr_len)) == -1) // 等待客户端连接{LOG(WARNING) << "等待客户端连接失败,正在重试...";sleep(1); // 等待1秒}ClientInfo client_info{myInetAddr(client_addr), client_fd}; // 客户端的IP和端口号,客户端的fdLOG(INFO) << "客户端" << client_info.client_inet_addr.Get_Ip() << ":" << client_info.client_inet_addr.Get_Port() << "连接成功";// 利用bind调整函数参数myThreadPool<task_t>::GetInstance()->AddTask(bind(&TcpServer::ClientCom, this, client_info)); // 将客户端的fd添加到线程池中,执行ClientCom函数}}void ClientCom(ClientInfo client_info) // 服务器直接向这个sockfd里面写数据即可实现通信{while (_isrunning) // 服务器运行时一直循环{bzero(_read_buffer, sizeof(_read_buffer)); // 对读写数组清0,数组名就是数组的首地址_write_buffer.clear(); // 对写数组清空// 会被阻塞在这个函数,直到收到数据,这个函数会将收到的数据写入_read_bufferssize_t n = recv(client_info.client_fd, _read_buffer, sizeof(_read_buffer) - 1, 0);if (n > 0){_read_buffer[n] = '\0'; // 读到的最后一个字符后面加上'\0',保证字符串安全if (strcmp(_read_buffer, "QUIT") == 0){LOG(INFO) << "客户端" << client_info.client_inet_addr.Get_Ip() << ":" << client_info.client_inet_addr.Get_Port() << "已断开连接";close(client_info.client_fd); // 直接关闭对应的fdreturn; // 该线程直接结束执行}_write_buffer = "客户端" + client_info.client_inet_addr.Get_Ip() + ":" + to_string(client_info.client_inet_addr.Get_Port()) + ":" + myCommand(_read_buffer).execute(); // 向客户端发送执行命令的结果send(client_info.client_fd, _write_buffer.c_str(), _write_buffer.size(), 0); // 向客户端发送执行命令的结果}}}void stop(){_isrunning = false; // 服务器停止,自动根据while退出循环}~TcpServer(){if (_listen_fd != -1){close(_listen_fd);LOG(INFO) << "已结束该端口的监听状态,服务端退出";_listen_fd = -1; // 防止二次调用多次关闭}}private:int _listen_fd; // 调用socket之后创建文件后返回的文件fd,用于监听连接bool _isrunning; // 记录当前服务端的运行状态myInetAddr _addr_server; // 服务端的IP和端口号string _write_buffer; // 服务器准备写出的信息char _read_buffer[max_size]; // 服务器读到的信息
};
②myCommand.hpp
#include <iostream>
#include <string>
using namespace std;namespace myCommandMoudle
{class myCommand // 用临时对象的方式实现命令执行{public:myCommand(const string &command): _command(command), _command_found(false){}string execute(){FILE *fp = popen(_command.c_str(), "r"); // 只读执行命令if (nullptr == fp){return "执行命令失败";}char tmp[1024];while (fgets(tmp, sizeof(tmp), fp) != nullptr){_result += tmp;_command_found = true;}if (!_command_found){_result = "command not found";}pclose(fp);return _result;}private:bool _command_found;string _command;string _result;};}
③TcpClientMain.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include "myCommon.hpp"using namespace std;int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 流式套接字,用于TCP连接
// 网络服务,sockaddr_in写入要连接的服务器的IP和端口号
sockaddr_in server; // 可以向指定IP和端口号发送信息void ClientQuit(int signal)
{string message = "QUIT";send(sockfd, message.c_str(), message.size(), 0);cout << "你已退出聊天室" << endl;exit(0);
}void *ReceiveMessage(void *args)
{while (1){char read_buffer[1024];int n = recv(sockfd, read_buffer, sizeof(read_buffer) - 1, 0); // 已经和服务器建立连接了,不需要再去获取服务器的IP和端口号read_buffer[n] = '\0';cerr << read_buffer << endl; // 输出接收到的信息,用错误流接收,后面专门用重定向设置一个聊天界面}
}// CS模式,client和server,client发送消息,server接收消息,服务器端永远不会主动发送消息,都是被动的
int main(int argc, char *argv[]) // 第二个参数是IP,第三个参数是端口号
{if (argc != 3){cerr << "共需要传入三个参数,一个IP,一个端口号" << endl;exit(CLIENT_ERROR); // 表示客户端错误}if (sockfd < 0){cerr << "客户端启动失败" << endl;exit(CLIENT_ERROR);}string server_ip = argv[1];uint16_t server_port = stoi(argv[2]);cout << "客户端启动成功" << endl;memset(&server, 0, sizeof(server)); // 用0初始化,0是char类型的0,即0x00server.sin_family = AF_INET;server.sin_port = htons(server_port); // 保证字节序server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 保证字节序signal(2, ClientQuit); // 捕获ctrl+c信号,退出程序,触发信号会执行ClientQuit函数,这个函数会向服务器发送QUIT信息,服务器会删除用户信息connect(sockfd, (const sockaddr *)(&server), sizeof(server)); // 连接服务器,TCP连接,需要先建立连接,才能发送和接收信息cout << "连接服务器成功" << endl;pthread_t tid;pthread_create(&tid, nullptr, ReceiveMessage, nullptr);while (true){cout << "请输入命令:";string message;getline(cin, message); // 获取信息// 不需要绑定socket,直接向文件写信息即可,client也有自己的属性,但IP和端口号不需要显式调用bind,客户端指定端口号可能会冲突,首次sendto会自动绑定// 客户端自动bind,一个端口号只能bind一次,一个进程可以绑定多个端口号send(sockfd, message.c_str(), message.size(), 0); // connect连接好了之后,就可以向服务器发送信息了,TCP协议}return 0;
}