Linux网络相关概念和重要知识(3)(TCP套接字编程、远程命令的实现)

发布于:2025-03-23 ⋅ 阅读:(23) ⋅ 点赞:(0)

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;
}