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和端口号,客户端的fd
LOG(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_buffer
ssize_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); // 直接关闭对应的fd
return; // 该线程直接结束执行
}
_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,即0x00
server.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;
}