【Linux网络与网络编程】04.TCP Socket编程

一、TCP Socket编程接口

// 创建套接字
int socket(int domain, int type, int protocol);
// 参数:
//		domain:域(协议家族),这里使用 AF_INET 表示进行网络编程
//		type:网络通信传输的类型,这里选择 SOCK_STREAM表示是使用TCP协议的面向数据报传递信息
//		protocol:这个参数目前置0即可
// 返回值:成功则返回一个文件描述符(所以创建套接字的本质就是创建了一个文件);失败则返回-1,并设置对应的错误码

// 填充网络信息并绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数:
//		sockfd:套接字文件描述符
//		addr:这是为了统一socket接口而定义的数据结构,其中的 sockaddr_in 是我们今天网络通信所要用到的,我们需要在绑定之前设置好它的协议家族,端口号和ip地址
//		addrlen:这是将addr的大小传递给内核,方便判断是哪种addr
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码

// TCP是面向连接的,连接成功后才能进行通信,因此要求TCP要随时随地等待被连接
// 监听
int listen(int sockfd, int backlog);
// 参数:
//		sockfd:被设置为监听状态的sockfd
//		backlog:最大连接个数
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码

// 获取新连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 参数:
//		sockfd:监听的sockfd
//		addr:请求连接的addr结构
//		addrlen:请求连接的addr结构的大小
// 返回值:成功返回一个用于服务的sockfd;失败则返回-1,并设置对应的错误码

// 客户端不需要bind,首次connect是被自动绑定
// 连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 参数:
//		sockfd:客户端想要去连接的sockfd
//		addr和addrlen:目标主机的addr结构
// 返回值:成功返回0;失败则返回-1,并设置对应的错误码

二、TCP Socket编程

2.1 EchoSever

这里我们来仿照UDP的Echo写一个EchoSever来快速熟悉接口。

//TCPSever.hpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "InetAddr.hpp"
#include "Log.hpp"

using namespace LogMudule;

const static uint16_t defaultport = 8888;

class TCPSever
{
public:
    TCPSever(uint16_t port = defaultport) : _addr(port)
    {
        // 创建套接字
        int n = _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket succeed";
        // 绑定
        n = ::bind(_listensockfd, _addr.NetAddr(), _addr.Len());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "bind succeed";
        // 开始监听
        n = ::listen(_listensockfd, 5);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "listen succeed";
    }
    void Run()
    {
        while(true)
        {
            //获取连接
            struct sockaddr_in connected_addr;
            socklen_t len=sizeof(connected_addr);
            int sockfd=::accept(_listensockfd,(struct sockaddr*)&connected_addr,&len);
            if(sockfd<0)
            {
                LOG(LogLevel::ERROR)<<"accept failed";
                continue;
            }
            
            InetAddr peer(connected_addr);
            LOG(LogLevel::INFO)<<"accept succeed connected is "<<peer.Addr() <<" sockfd is "<<sockfd;

            //既然可以获取到信息了,那么下一步就是提供服务了
            Service(sockfd);

            //完成之后就要关闭sockfd
            ::close(sockfd);
        }
    }
    ~TCPSever() 
    {
        ::close(_listensockfd);
    }
private:
    void Service(int sockfd)
    {
        char buff[1024];
        while(true)
        {
            //TCP是面向字节流的,故而可以采用文件的接口进行读取和写入
            int n=::read(sockfd,buff,sizeof(buff)-1);
            if(n>0)
            {
                buff[n]=0;
                std::string message="Client Say@";
                message+=buff;
                int m=::write(sockfd,message.c_str(),message.size());
                if(m<0) 
                {
                    LOG(LogLevel::ERROR)<<"write failed";
                }
            }
            else if(n==0)
            {
                //表示读到了文件末尾
                LOG(LogLevel::INFO)<<"Client Quit……";
                break;
            }
            else
            {
                LOG(LogLevel::ERROR)<<"read error";
                break;
            }
        }
    }

private:
    int _listensockfd;
    InetAddr _addr;
};
//TCPSever.cc
#include "TCPSever.hpp"
int main()
{
    std::unique_ptr<TCPSever> ts_ptr=std::make_unique<TCPSever>();
    ts_ptr->Run();
    return 0;
}

再来完成我们的客户端代码:

// TCPClient.hpp
#pragma once
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogMudule;
const static std::string defaultip="127.0.0.1";
const static int defaultport=8888;

class TCPClient
{
public:
    TCPClient(std::string ip,uint16_t port):_dst_addr({ip,port})
    {
        //创建套接字
        _sockfd=::socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd<0)
        {
            LOG(LogLevel::FATAL)<<"socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO)<<"socket succeed";
        //不需要绑定
    }
    void Run()
    {
        cout<<_sockfd<<endl;
        int n=::connect(_sockfd,_dst_addr.NetAddr(),_dst_addr.Len());
        if(n<0)
        {
            LOG(LogLevel::ERROR)<<"connect failed";
            exit(3);
        }
        LOG(LogLevel::INFO)<<"connect succeed";
        while(true)
        {

            std::string message;
            std::getline(std::cin,message);
            cout<<"message is:"<<message<<endl;
            ::write(_sockfd,message.c_str(),message.size());
            
            char buff[1024];
            n=::read(_sockfd,buff,sizeof(buff)-1);
            if(n>0)
            {
                buff[n]=0;
                std::cout<<buff<<std::endl;
            }
        }
    }
    ~TCPClient()
    {
        ::close(_sockfd);
    }

private:
    int _sockfd;
    InetAddr _dst_addr;
};
//TCOClient.cc
#include <memory>
#include "TCPClient.hpp"

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cout<<"Usgae Error"<<std::endl;
        exit(-1);
    }
    std::string ip=argv[1];
    uint16_t port=std::stoi(argv[2]);
    std::unique_ptr<TCPClient> c_ptr=std::make_unique<TCPClient>(ip,port);
    c_ptr->Run();
    return 0;
}

这段客户端的代码我们就写好了,简单的测试过后可以发现是可以的。本篇博客采用telnet来充当客户端进行测试:
在这里插入图片描述
可以见得我们的服务端的代码逻辑也是正确的,接下来我们要实现多线程和多进程的EchoSever

进程分离管理:

//……
void Run()
{
    while (true)
    {
        // 2. 进程分离版本,子进程执行任务,父进程来进行监听
        // 获取连接
        struct sockaddr_in connected_addr;
        socklen_t len = sizeof(connected_addr);
        int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "accept failed";
            continue;
        }

        InetAddr peer(connected_addr);
        LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;
        int id = fork();
        if (id == 0)
        {
            // 子进程
            ::close(_listensockfd);
            // 子进程再次创建子进程,子进程返回,避免父进程阻塞,孙子进程为孤儿进程,被OS领养
            if(fork()>0)    exit(0);
            Service(sockfd);
            exit(0);
        }
        else
        {
            // 父进程
            ::close(sockfd);
            int rid=::waitpid(id,nullptr,0);
            if(rid<0)
            {
                LOG(LogLevel::ERROR) << "waitpid failed";
            }
        }
    }
}
//……

线程分离管理:

//……
// 3. 线程分离管理
struct ThreadData
{
    int _sockfd;
    TCPSever *_self;
};
static void *Handler(void *args)
{
    pthread_detach(pthread_self());
    ThreadData* data=(ThreadData*)args;
    data->_self->Service(data->_sockfd);
    return nullptr;
}
//……
void Run()
{
	while (true)
	{
	    // 3.线程管理版本的
	    // 获取连接
	    struct sockaddr_in connected_addr;
	    socklen_t len = sizeof(connected_addr);
	    int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
	    if (sockfd < 0)
	    {
	        LOG(LogLevel::ERROR) << "accept failed";
	        continue;
	    }
	
	    InetAddr peer(connected_addr);
	    LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;
	   
	    ThreadData* data=new ThreadData;
	    data->_sockfd=sockfd;
	    data->_self=this;
	
	    pthread_t tid; 
	    pthread_create(&tid, nullptr, Handler, data);
	}
}

线程池管理:

//……
void Run()
{
    while (true)
    {
        // 4.线程池管理版本
        struct sockaddr_in connected_addr;
        socklen_t len = sizeof(connected_addr);
        int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
        if (sockfd < 0)
        {
            LOG(LogLevel::ERROR) << "accept failed";
            continue;
        }

        InetAddr peer(connected_addr);
        LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;

        //绑定生成任务
        task_t task=std::bind(&TCPSever::Service,this,sockfd);
        ThreadPool<task_t> ::GetInstance()->Enqueue(task);
    }
}
//……

2.2 远程命令执行

//command.hpp
#pragma once
#include <string>
#include <cstring>
#include "Log.hpp"

using namespace LogMudule;

class Command
{
public:
    std::string Excute(const std::string &command)
    {
        // 相当于pipe + excl
        FILE *fp = popen(command.c_str(), "r");
        if (fp == nullptr)
            return std::string();
        char buffer[1024];
        std::string result;
        while (fgets(buffer, sizeof(buffer), fp))
        {
            result += buffer;
        }
        pclose(fp);
        return result;
    }
};
//TCPSever.hpp
#pragma once
#include <string>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"

using namespace LogMudule;
using namespace ThreadPoolModual;

const static uint16_t defaultport = 8888;

using task_t = std::function<void()>;
using command_t = std::function<std::string(const std::string)>;

class TCPSever
{
    void Service(int sockfd)
    {
        char buff[1024];
        while (true)
        {

            // TCP是面向字节流的,故而可以采用文件的接口进行读取和写入
            int n = ::read(sockfd, buff, sizeof(buff) - 1);
            if (n > 0)
            {
                //网络传输中以\r\n结尾
                buff[n-2] = 0;
                
                std::string message = _command(buff);
                int m = ::send(sockfd, message.c_str(), message.size(),0);
                if (m < 0)
                {
                    LOG(LogLevel::ERROR) << "write failed";
                }
            }
            else if (n == 0)
            {
                // 表示读到了文件末尾
                LOG(LogLevel::INFO) << "Client Quit……";
                break;
            }
            else
            {
                LOG(LogLevel::ERROR) << "read error";
                break;
            }
        }
    }

public:
    TCPSever(command_t command, uint16_t port = defaultport) : _command(command), _addr(port)
    {
        // 创建套接字
        int n = _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "socket failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket succeed";
        // 绑定
        n = ::bind(_listensockfd, _addr.NetAddr(), _addr.Len());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "bind succeed";
        // 开始监听
        n = ::listen(_listensockfd, 5);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen failed";
            exit(1);
        }
        LOG(LogLevel::INFO) << "listen succeed";
    }

    void Run()
    {
        while (true)
        {
            // 4.远程执行任务
            struct sockaddr_in connected_addr;
            socklen_t len = sizeof(connected_addr);
            int sockfd = ::accept(_listensockfd, (struct sockaddr *)&connected_addr, &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::ERROR) << "accept failed";
                continue;
            }

            InetAddr peer(connected_addr);
            LOG(LogLevel::INFO) << "accept succeed connected is " << peer.Addr() << " sockfd is " << sockfd;

            // 绑定生成任务
            task_t task = std::bind(&TCPSever::Service, this, sockfd);
            ThreadPool<task_t>::GetInstance()->Enqueue(task);
        }
    }
    ~TCPSever()
    {
        ::close(_listensockfd);
    }

private:
    int _listensockfd;
    InetAddr _addr;

    command_t _command;
};

三、Windows下的TCP Socket

#include <winsock2.h>
#include <iostream>
#include <string>
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
std::string serverip = "62.234.18.77"; // 填写你的云服务器 ip
uint16_t serverport = 8888; // 填写你的云服务开放的端口号
int main()
{
	WSADATA wsaData;
	int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (result != 0)
	{
		std::cerr << "WSAStartup failed: " << result << std::endl;
		return 1;
	}
	SOCKET clientSocket = socket(AF_INET, SOCK_STREAM,
		IPPROTO_TCP);
	if (clientSocket == INVALID_SOCKET)
	{
		std::cerr << "socket failed" << std::endl;
		WSACleanup();
		return 1; 
	}
	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(serverport);
	serverAddr.sin_addr.s_addr = inet_addr(serverip.c_str());
	result = connect(clientSocket, (SOCKADDR*)&serverAddr,
		sizeof(serverAddr));
	if (result == SOCKET_ERROR)
	{
		std::cerr << "connect failed" << std::endl;
		closesocket(clientSocket);
		WSACleanup();
		return 1;
	}
	while (true)
	{
		std::string message;
		std::cout << "Please Enter@ ";
		std::getline(std::cin, message);
		if (message.empty()) continue;
		send(clientSocket, message.c_str(), message.size(), 0);
		char buffer[1024] = { 0 };
		int bytesReceived = recv(clientSocket, buffer,
			sizeof(buffer) - 1, 0);
		if (bytesReceived > 0)
		{
			buffer[bytesReceived] = '\0'; // 确保字符串以 null 结尾
			std::cout << "Received from server: " << buffer <<
				std::endl;
		}
		else
		{
			std::cerr << "recv failed" << std::endl;
		}
	}
	closesocket(clientSocket);
	WSACleanup();
	return 0;
}

最后附上跨平台测试截图:
在这里插入图片描述

四、connect 断线重连

客户端会面临服务器崩溃的情况,我们可以试着写一个客户端重连的代码,模拟并理解一些客户端行为,比如游戏客户端等。

#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

enum class Status
{
    NEW,
    CONNECTING,
    CONNECTED,
    DISCONNECTED,
    CLOSED
};

class ClientConnect
{
public:
    ClientConnect(std::string severip, uint16_t severport)
        : _sockfd(-1),
          _severip(severip),
          _severport(severport),
          _retry_intried(5),
          _retry_interval(1),
          _status(Status::NEW)
    {
    }
    void Connect()
    {
        // 申请sockfd
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket failed!" << std::endl;
            exit(1);
        }
        // 连接
        struct sockaddr_in sever;
        bzero(&sever, sizeof(sever));
        sever.sin_family = AF_INET;
        sever.sin_port = htons(_severport);
        sever.sin_addr.s_addr = inet_addr(_severip.c_str());

        int n = connect(_sockfd, (struct sockaddr *)&sever, sizeof(sever));
        if (n < 0)
        {
            DisConnect();
            _status = Status::DISCONNECTED;
            return;
        }
        _status = Status::CONNECTED;
    }
    void Process()
    {
        while(true)
        {
            std::string message = "hello sever!";
            ssize_t n = write(_sockfd,message.c_str(),message.size());
            if(n > 0)
            {
                char buffer[1024];
                ssize_t m = read(_sockfd,buffer,sizeof(buffer));
                if(m > 0)
                {
                    buffer[m] = 0;
                    std::cout << buffer << std::endl;
                }
                else
                {
                    _status = Status::DISCONNECTED;
                    break;
                }
            }
            else
            {
                _status = Status::CLOSED;
                break;
            }
            sleep(1);
        }
    }
    void Reconnect()
    {
        _status = Status::CONNECTING;
        int cnt = 0;
        while(true)
        {
            Connect();
            if(_status == Status::CONNECTED)
                return;
            sleep(_retry_interval);
            cnt++;
            if(cnt > _retry_intried)
            {
                _status = Status::CLOSED;
                return;
            }
        }
    }
    void DisConnect()
    {
        if(_sockfd != -1)
        {
            close(_sockfd);
            _status = Status::CLOSED;
            _sockfd = -1;
        }
    }
    ~ClientConnect()
    {
        DisConnect();
    }
    Status ConnectStatus()
    {
        return _status;
    }

private:
    int _sockfd;          // 申请的sockfd
    uint16_t _severport;  // 服务端端口号
    std::string _severip; // 服务端IP
    int _retry_intried;   // 重试次数
    int _retry_interval;  // 连接间隔
    Status _status;       // 状态
};

class TCPClient
{
public:
    TCPClient(std::string severip, uint16_t severport) : _cc(severip, severport)
    {
    }
    void Excute()
    {
        while (true)
        {
            switch (_cc.ConnectStatus())
            {
            case Status::NEW:
                _cc.Connect();
                break;
            case Status::CONNECTED:
                std::cout << "连接成功,正在通信……" << std::endl;
                _cc.Process();
                break;
            case Status::DISCONNECTED:
                std::cout << "连接失败,正在重试……" << std::endl;
                _cc.Reconnect();
                break;
            case Status::CLOSED:
                std::cout << "重连失败,正在退出……" << std::endl;
                _cc.DisConnect();
                return;
            default:
                // do nothing
                break;
            }
        }
    }
    ~TCPClient()
    {
    }
private:
    ClientConnect _cc;
};



int main(int argc,char* argv[])
{   
    if(argc!=3)
    {
        std::cout << "Usage Error!" <<std::endl;
        exit(-1);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    TCPClient tp(ip,port);
    tp.Excute();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值