socket套接字——TCP协议

目录

一、TCP协议相关函数

1.socket、bind函数

2.listen函数

3.accept函数

4.connect函数

二、实现TCP通信

1.服务端实现

(1)服务端类

(2)服务器状态的显示

(3)初始化服务端

(4)启动服务器

(5)IO任务函数

(6)析构函数

(7)main函数

(8)总代码

2.客户端实现

(1)客户端类

(2)初始化客户端

(3)启动客户端

(4)IO任务函数

(5)main函数

(6)总代码

三、代码改造

1.多进程版本

2.多线程版本

3.线程池版本

四、最终代码

1.线程池等准备类

2.服务端

3.客户端


一、TCP协议相关函数

TCP与UDP协议使用的套接字接口比较相似,但TCP需要使用的接口更多,细节也会更多。

1.socket、bind函数

这两个函数在该篇博文第二部分socket套接字的第一部分有介绍。socket套接字——UDP协议_聪明的骑士的博客-CSDN博客

2.listen函数

int listen(int sockfd, int backlog);

头文件:sys/socket.h

功能:设置该文件描述符为监听状态。

参数:int sockfd表示之前使用socket()返回的文件描述符sockfd。

int backlog这个参数以后再说,现在说不明白。

返回值:成功返回另一个用于通信的文件描述符,失败返回-1并设置错误码errno。

3.accept函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

头文件:sys/socket.h

功能:将网络中发来的连接申请进行连接。

参数:int sockfd表示之前使用listen()返回的文件描述符。

struct sockaddr *addr是一个输出型参数,可以把与该进程连接的进程的网络信息填入其中。

socklen_t *addrlen是struct sockaddr的大小。

返回值:成功返回0,失败返回-1并设置错误码errno。

4.connect函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

头文件:sys/socket.h、sys/types.h

功能:将网络中发来的连接申请进行连接。

参数:int sockfd表示之前使用listen()返回的文件描述符。

struct sockaddr *addr是一个输出型参数,可以把与该进程连接的进程的网络信息填入其中。

socklen_t *addrlen是struct sockaddr的大小。

返回值:成功返回0,失败返回-1并设置错误码errno。

二、实现TCP通信

下面让我们实现一个TCP通信的服务器和客户端。

1.服务端实现

(1)服务端类

服务端类的构建与前面基本一致,全部构建在server.hpp。但成员变量只包括一个文件描述符istensock,还有一个端口号_port,可以设为缺省值。

我们依旧自己定义枚举的错误码。

static const uint16_t given_port = 8080;
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};
class udpserver
{
public:
    //构造函数
    tcpserver(const uint16_t& port = given_port)
        :_port(port)
        ,_sockfd(-1)
    {}

    //初始化服务端进程
    void initserver()
    {
    }
    
    //启动服务端进程,服务端需要一直运行,所以是一个死循环程序
    void start()
    {
    }
    
    ~tcpserver()
    {}

private:
    uint16_t _port;//服务端进程的端口号
    int _sockfd;//socket返回的文件描述符
};

(2)服务器状态的显示

由于网络的操作步骤多,查找bug困难,所以我们在程序运行到某一个阶段时都告诉我们当前的操作是完成了还是没完成,新建一个log.hpp。

按照当前程序运行的状态,在头文件中定义五个宏:

DEBUG无意义,NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误。

每次这个logmessage函数都把格式化的信息达到屏幕上,告诉我们程序的状态,当然我们也可以把它同时写到文件中,这也是它的一个简单的日志。

va_list是一个可变参数列表,可以去网上查找相关使用。

socket套接字——TCP协议_第1张图片

(3)初始化服务端

initserver()用于初始化服务端,由于TCP属于可靠传输,它对于网络连接的可靠性比较苛刻,所以它的服务端初始化步骤也会多一些。二者的服务端流程如下:

UDP:socket函数获取文件描述符fd->bind函数绑定IP

TCP:socket获取一个文件描述符fd1->bind绑定IP->用listen函数设置fd1为监听状态

我们的函数就是按照桑面TCP描述的顺序socket,bind,listen,当函数发生问题时打印错误信息并以对应错误码退出即可。

注意TCP协议是面向字节流的,socket的选项改为SOCK_STREAM。

socket套接字——TCP协议_第2张图片

(4)启动服务器

启动服务器需要使用accept函数等待客户端的连接,无连接请求会阻塞等待,出现连接请求则会返回一个用于通信的文件描述符 。

然后就是处理IO任务了,注意处理完成后要归还通信的文件描述符。

socket套接字——TCP协议_第3张图片

(5)IO任务函数

这个函数负责将客户端发来的数据打印到自己的屏幕上,然后再发回客户端。

由于网络接口集成在了文件接口中,所以使用read和write进行收发信息。

socket套接字——TCP协议_第4张图片

(6)析构函数

按道理监听的文件描述符也应当在不用时释放,但我们选择不释放。

(7)main函数

main函数可以直接使用以前的main函数实现。

socket套接字——TCP协议_第5张图片

(8)总代码

server.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include 
#include

#include"Task.hpp"
#include"Thread.hpp"
#include"Threadpool.hpp"

static const uint16_t given_port = 8080;
static const int given_backlog = 5;

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

using namespace std;

class tcpserver;//前置声明
class Threaddata
{
public:
    Threaddata(int sock, tcpserver* pdata)
        :_sock(sock)
        ,_pdata(pdata)
    {}
    int _sock;
    tcpserver* _pdata;
};

class tcpserver
{
public:
    //构造函数
    tcpserver(const uint16_t& port = given_port)
        :_port(port)
        ,_listensock(-1)
    {}

    //初始化服务端进程
    void initserver()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(_listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到

        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到

        //_listensock用于监听,不是用于通信的端口号
        //listen函数可设置socket为监听模式
        if(listen(_listensock, given_backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(NORMAL, "listen socket success");
    }

    //线程池版本解耦程序,需要将该函数转移到Task.hpp中
    void serviceIO(int sock)
    {
        char buffer[1024];//设置缓冲区
        while(1)
        {
            size_t n = read(sock, buffer, sizeof(buffer)-1);//减一是为了留出一个\0
            if(n > 0)//读到数据了
            {
                buffer[n] = '\0';//把发来的\n覆盖掉
                cout << "get data:" << buffer << endl;
                std::string out();
                out += "buffer";
                write(sock, out.c_str(), out.size());
            }
            else
            {
                logmessage(NORMAL, "Client quit, break the link.");
                //n == 0服务端收不到数据了,证明客户端退出了,连接断了,客户端也断开连接
                break;
            }
        }
    }
    
    //启动服务端进程,普通版本
    void start()
    {
        while(1)
        {
            struct sockaddr_in peer;//储存本地网络信息
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
            //如果没有客户端连接服务端,则accept会阻塞等待新连接
            //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
            if(sock < 0)
            {
                logmessage(ERROR, "accept fail");//接收新文件描述符失败
                continue;//重新回到头接收
            }
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
            //不断处理IO任务
            serviceIO(sock);
            //退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
            close(sock);
            //服务端释放通信的文件描述符,但不释放监听的文件描述符
            //服务端进程可以继续使用该监听文件描述符阻塞等待下一个客户端连接
        }
    }

    //启动服务端进程,线程池版本
    void start()
    {
        //初始化线程池
        ThreadPool::GetInstance()->run();
        logmessage(NORMAL, "Thread init success");
        while(1)
        {
            struct sockaddr_in peer;//储存本地网络信息
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
            //如果没有客户端连接服务端,则accept会阻塞等待新连接
            //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
            if(sock < 0)
            {
                logmessage(ERROR, "accept fail");//接收新文件描述符失败
                continue;//重新回到头接收
            }
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
            ThreadPool::GetInstance()->push(Task(sock, serviceIO));
        }
    }

    //按道理不释放监听文件描述符也是一种资源泄漏,但是服务器进程大部分需要长期运行
    //只有进程出问题了进程才会退出,而当进程退出时,它占用的资源也归还了操作系统,所以不释放也无所谓
    ~tcpserver()
    {}
private:
    uint16_t _port;//服务端进程的端口号
    int _listensock;//监听文件描述符
};

server.cc

#include"log.hpp"
#include"server.hpp"
#include

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s local_port\n\n",proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 2)//如果没输入端口号,argc保存的命令参数只有一个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr p(new tcpserver(port));

    p->initserver();
    p->start();

    return 0;
}

2.客户端实现

(1)客户端类

客户端类的构建与前面基本一致,成员变量不太一样,依旧自己定义枚举的错误码。

#define NUM 1024
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    CONNECT_ERROR
};

class tcpclient
{
public:
    //构造函数
    tcpclient(const std::string& ip, const uint16_t& port)
        :_ip(ip)
        ,_port(port)
        ,_sock(-1)
    {}

    void initclient()
    {}

    //启动服务端进程,服务端需要一直运行,所以是一个死循环程序
    void run()
    {}

    //析构函数要释放不使用的文件描述符
    ~tcpclient()
    {
        if( _sock >= 0)
            close(_sock);
    }
private:
    int _sock;//套接字文件描述符
    std::string _ip;//服务器IP地址
    uint16_t _port;//服务器的端口号
};

(2)初始化客户端

initclient()用于初始化客户端,只需要创建套接字就可以了。

socket套接字——TCP协议_第6张图片

(3)启动客户端

客户端需要使用connect函数向服务端发连接请求,连接成功则循环发送并接收数据。

socket套接字——TCP协议_第7张图片

(4)IO任务函数

这个函数负责将客户端发来的数据打印到自己的屏幕上,然后再发回客户端。

由于网络接口集成在了文件接口中,所以使用read和write进行收发信息。

socket套接字——TCP协议_第8张图片

(5)main函数

main函数也可以直接使用以前的main函数实现。

socket套接字——TCP协议_第9张图片

(6)总代码

client.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define NUM 1024
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    CONNECT_ERROR
};

class tcpclient
{
public:
    //构造函数
    tcpclient(const std::string& ip, const uint16_t& port)
        :_ip(ip)
        ,_port(port)
        ,_sock(-1)
    {}

    void initclient()
    {
        //创建套接字,创建失败打印错误原因
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock == -1)
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
        //客户端不需要显式绑定,该工作交给操作系统完成
    }

    //启动客户端进程
    void run()
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体

        //客户端连接服务器
        if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
        {
            logmessage(FATAL, "connect error");//connect失败属于最严重的错误
        }
        else
        {
            std::string msg;
            while(1)
            {
                //向服务器发送数据
                printf("Please enter#");
                getline(std::cin, msg);
                write(_sock, msg.c_str(), msg.size());
                //接收服务器发回的数据
                char buffer[1024];
                int n = read(_sock, buffer, sizeof(buffer)-1);
                if(n > 0)
                {
                    buffer[n] = 0;
                    printf("server return$%s\n", buffer);
                }
                else
                {
                    //read返回0,证明服务端关闭,客户端也要关闭
                    break;
                }
            }
        }
    }

    //析构函数要释放不使用的文件描述符
    ~tcpclient()
    {
        if( _sock >= 0)
            close(_sock);
    }
private:
    int _sock;//套接字文件描述符
    std::string _ip;//服务器IP地址
    uint16_t _port;//服务器的端口号
};

client.cc

#include"log.hpp"
#include"client.hpp"

using namespace std;

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[2]);
    string ip = argv[1];
    
    unique_ptr p(new tcpclient(ip, port));

    p->initclient();
    p->run();

    return 0;
}

三、代码改造

1.多进程版本

我们使用创建多进程的方式处理IO任务,只需要将start函数改变即可,实现的原理可以看注释。

socket套接字——TCP协议_第10张图片

2.多线程版本

我们使用创建多线程的方式处理IO任务,也只需要将start函数改变即可,实现的原理可以看注释。

socket套接字——TCP协议_第11张图片

3.线程池版本

线程池使用的就是前面的线程池(单例模式),线程池需要改变,printf注释掉,改为data()

socket套接字——TCP协议_第12张图片

还有start也要改,而main函数不用改。

四、最终代码

下面的代码包括了所有类型的实现,通过注释不同的字段,就可以实现不同的版本。(现在的为线程池版本)

1.线程池等准备类

log.hpp

#pragma once
#include
#include
#include
#include
#include

//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

//将运行等级转换为字符串
const char* to_string(int level)
{
    switch(level)
    {
        case(DEBUG):
            return "DEBUG";
        case(NORMAL):
            return "NORMAL";
        case(WARNING):
            return "WARNING";
        case(ERROR):
            return "ERROR";
        case(FATAL):
            return "FATAL";
        default:
            return nullptr;
    }
}

//将固定格式的日志输出到屏幕上
//第一个参数是等级,第二个参数是需要输出的字符串
void logmessage(int level, const char* format, ...)
{
    char logprefix[1024];//
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串
    
    char logcontent[1024];//
    va_list arg;//可变参数列表
    va_start(arg, format);//
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    std::cout << logprefix << logcontent << std::endl;
}

Threadpool.hpp

#include
#include
#include
#include
#include

#define THREAD_NUM 10

懒汉模式///
//前面加上声明
template 
class ThreadPool;

//线程数据类
template 
class ThreadData
{
public:
    ThreadPool* _pthreadpool;//线程池的this指针
    std::string _threadname;//线程的名字
    //构造函数
    ThreadData(ThreadPool* tp, std::string name)
        :_pthreadpool(tp)
        ,_threadname(name)
    {}
};

//线程池
template 
class ThreadPool
{
public:
    //析构函数
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_cmutex);//销毁消费互斥锁
        pthread_mutex_destroy(&_pmutex);//销毁生产互斥锁
        pthread_cond_destroy(&_cond);//销毁条件变量
        //销毁多个线程
        for(size_t i = 0; i < _num; ++i)
        {
            _threads[i]->join();
            delete _threads[i];
        }
    }

    //将所有线程启动
    void run()
    {
        for(size_t i = 0; i < _num; ++i)
        {
            //由于线程函数需要使用线程池类内的函数和每一个线程的名字,所以将它们合起来构造一个线程数据类传递给线程操作函数
            ThreadData* p = new ThreadData(this, _threads[i]->threadname());
            _threads[i]->start(handler_task, (void*)p);//这里也可以设计一个类
            std::string s(p->_threadname);
            s += " start...\n";
            std::cout << s;
        }
    }

    //向线程池推送任务
    void push(const T& data)
    {
        pthread_mutex_lock(&_pmutex);
        _task_queue.push(data);
        pthread_cond_signal(&_cond);
        pthread_mutex_unlock(&_pmutex);
    }

    //消费线程取任务,加锁解锁已经在消费线程处理函数里进行了,不需要注意线程安全
    T pop()
    {
        T data = _task_queue.front();
        _task_queue.pop();
        return data;
    }

    //静态成员函数需要访问的非静态成员接口
    bool isQueueEmpty() {return _task_queue.empty();}//判断任务队列是否为空
    void lockQueue() {pthread_mutex_lock(&_cmutex);}//给任务队列加锁
    void unlockQueue() {pthread_mutex_unlock(&_cmutex);}//给任务队列解锁
    void threadWait() {pthread_cond_wait(&_cond,&_cmutex);}//将线程放入条件变量的等待队列中

    //获取单例对象指针的接口
    static ThreadPool* GetInstance()
    {
        if(_pobj == nullptr)
        {
            _singlelock.lock();
            if(_pobj == nullptr)
            {
                _pobj = new ThreadPool();
            }
            _singlelock.unlock();
        }
        return _pobj;
    }

private:
    //构造函数
    ThreadPool(int num = THREAD_NUM)
    :_num(num)
    {
        pthread_mutex_init(&_cmutex, nullptr);//初始化消费互斥锁
        pthread_mutex_init(&_pmutex, nullptr);//初始化生产互斥锁
        pthread_cond_init(&_cond, nullptr);//初始化条件变量
        //创建多个线程
        for(size_t i = 0; i < _num; ++i)
        {
            _threads.push_back(new Thread());
        }
    }

    ThreadPool(const ThreadPool& tp) = delete;//禁止拷贝构造
    ThreadPool& operator=(const ThreadPool& tp) = delete;//禁止赋值运算符重载

    //消费线程的处理函数
    static void* handler_task(void* args)
    {
        ThreadData* p = (ThreadData*)args;
        while(1)
        {
            p->_pthreadpool->lockQueue();
            //如果任务队列为空,消费者进程会被加入到条件变量的阻塞队列中
            while(p->_pthreadpool->isQueueEmpty())
            {
                p->_pthreadpool->threadWait();
            }
            T data = p->_pthreadpool->pop();
            p->_pthreadpool->unlockQueue();
            
            data();
            //printf("%s接受了任务%s并处理完成,结果为:%s\n", p->_threadname.c_str(), 
            //data.show_task().c_str(), data().c_str());
        }
        delete p;
        return nullptr;
    }

    int _num;//维护的线程数量
    std::vector _threads;//管理多个线程对象的容器
    std::queue _task_queue;//任务队列
    pthread_mutex_t _cmutex;//消费者互斥锁
    pthread_cond_t _cond;//条件变量
    pthread_mutex_t _pmutex;//生成任务时的互斥锁

    static ThreadPool* _pobj;//静态单例对象
    static std::mutex _singlelock;
    //由于单例的建立有线程安全问题,所以需要加锁,这里为了方便使用了C++11提供的互斥锁
};

template
ThreadPool* ThreadPool::_pobj = nullptr;//ThreadPool单例先不初始化
template
std::mutex ThreadPool::_singlelock;//初始化锁

2.服务端

server.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include 
#include

#include"Task.hpp"
#include"Thread.hpp"
#include"Threadpool.hpp"

static const uint16_t given_port = 8080;
static const int given_backlog = 5;

enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    LISTEN_ERROR
};

using namespace std;

class tcpserver;//前置声明
class Threaddata
{
public:
    Threaddata(int sock, tcpserver* pdata)
        :_sock(sock)
        ,_pdata(pdata)
    {}
    int _sock;
    tcpserver* _pdata;
};

class tcpserver
{
public:
    //构造函数
    tcpserver(const uint16_t& port = given_port)
        :_port(port)
        ,_listensock(-1)
    {}

    //初始化服务端进程
    void initserver()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字
        if(_listensock < 0)//创建套接字失败打印错误原因
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _listensock);//创建套接字成功,打印让用户观察到

        struct sockaddr_in local;//储存本地网络信息
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息
        {
            logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误
            exit(BIND_ERROR);//退出
        }
        logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到

        //_listensock用于监听,不是用于通信的端口号
        //listen函数可设置socket为监听模式
        if(listen(_listensock, given_backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logmessage(FATAL, "listen socket error");
            exit(LISTEN_ERROR);
        }
        logmessage(NORMAL, "listen socket success");
    }

    // //线程池版本解耦程序,需要将该函数转移到Task.hpp中
    // void serviceIO(int sock)
    // {
    //     char buffer[1024];//设置缓冲区
    //     while(1)
    //     {
    //         size_t n = read(sock, buffer, sizeof(buffer)-1);//减一是为了留出一个\0
    //         if(n > 0)//读到数据了
    //         {
    //             buffer[n] = '\0';//把发来的\n覆盖掉
    //             cout << "get data:" << buffer << endl;
    //             std::string out();
    //             out += "buffer";
    //             write(sock, out.c_str(), out.size());
    //         }
    //         else
    //         {
    //             logmessage(NORMAL, "Client quit, break the link.");
    //             //n == 0服务端收不到数据了,证明客户端退出了,连接断了,客户端也断开连接
    //             break;
    //         }
    //     }
    // }

    //启动服务端进程,普通版本
    // void start()
    // {
    //     while(1)
    //     {
    //         struct sockaddr_in peer;//储存本地网络信息
    //         socklen_t len = sizeof(peer);
    //         int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
    //         //如果没有客户端连接服务端,则accept会阻塞等待新连接
    //         //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
    //         if(sock < 0)
    //         {
    //             logmessage(ERROR, "accept fail");//接收新文件描述符失败
    //             continue;//重新回到头接收
    //         }
    //         logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
    //         //不断处理IO任务
    //         serviceIO(sock);
    //         //退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
    //         close(sock);
    //         //服务端释放通信的文件描述符,但不释放监听的文件描述符
    //         //服务端进程可以继续使用该监听文件描述符阻塞等待下一个客户端连接
    //     }
    // }

    // //启动服务端进程,多进程版本
    // void start()
    // {
    //     while(1)
    //     {
    //         struct sockaddr_in peer;//储存本地网络信息
    //         socklen_t len = sizeof(peer);
    //         int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
    //         //如果没有客户端连接服务端,则accept会阻塞等待新连接
    //         //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
    //         if(sock < 0)
    //         {
    //             logmessage(ERROR, "accept fail");//接收新文件描述符失败
    //             continue;//重新回到头接收
    //         }
    //         logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
    //         //创建子进程
    //         pid_t id = fork();
    //         if(id == 0)
    //         {
    //             //由于子进程也继承了父进程的监听套接字,而监听套接字只需要一个,所以需要关闭
    //             close(_listensock);
    //             if(fork() > 0)
    //             {
    //                 //子进程再次创建子进程,fork对父进程(当前是服务器的子进程)返回子进程pid,对子进程(当前是服务器的孙子进程)返回0
    //                 exit(0);//服务器子进程退出
    //             }
    //             //服务器的孙子进程执行IO处理
    //             //不断处理IO任务
    //             serviceIO(sock);
    //             //退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
    //             close(sock);
    //             //退出孙子进程,由于它是孤儿进程,所以会被操作系统自行回收
    //             exit(0);
    //         }
    //     }
    // }

    //启动服务端进程,多线程版本,注意编译要加-lpthread
    static void* thread_routine(void* args)
    {
        pthread_detach(pthread_self());//将该线程分离,执行完毕后操作系统自动回收
        Threaddata* p = (Threaddata*)args;
        p->_pdata->serviceIO(p->_sock);
        //退出该函数时,客户端已经退出,需要将进行网络传输的文件描述符释放,否则会引起资源泄露
    }

    //先看这里
    void start()
    {
        while(1)
        {
            struct sockaddr_in peer;//储存本地网络信息
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
            //如果没有客户端连接服务端,则accept会阻塞等待新连接
            //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
            if(sock < 0)
            {
                logmessage(ERROR, "accept fail");//接收新文件描述符失败
                continue;//重新回到头接收
            }
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
            //创建新线程
            pthread_t tid;
            Threaddata* p = new Threaddata(sock, this);
            pthread_create(&tid, nullptr, thread_routine, p);//创建线程并开始执行
        }
    }

    //启动服务端进程,线程池版本
    void start()
    {
        //初始化线程池
        ThreadPool::GetInstance()->run();
        logmessage(NORMAL, "Thread init success");
        while(1)
        {
            struct sockaddr_in peer;//储存本地网络信息
            socklen_t len = sizeof(peer);
            int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
            //如果没有客户端连接服务端,则accept会阻塞等待新连接
            //如果有客户端需要连接服务端,则返回一个用于网络通信的描述符sock
            if(sock < 0)
            {
                logmessage(ERROR, "accept fail");//接收新文件描述符失败
                continue;//重新回到头接收
            }
            logmessage(NORMAL, "accept a new link");//接收新文件描述符成功
            
            ThreadPool::GetInstance()->push(Task(sock, serviceIO));
        }
    }

    //按道理不释放监听文件描述符也是一种资源泄漏,但是服务器进程大部分需要长期运行
    //只有进程出问题了进程才会退出,而当进程退出时,它占用的资源也归还了操作系统,所以不释放也无所谓
    ~tcpserver()
    {}
private:
    uint16_t _port;//服务端进程的端口号
    int _listensock;//监听文件描述符
};

server.cc

#include"log.hpp"
#include"server.hpp"
#include

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s local_port\n\n",proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 2)//如果没输入端口号,argc保存的命令参数只有一个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[1]);
    unique_ptr p(new tcpserver(port));

    p->initserver();
    p->start();

    return 0;
}

3.客户端

client.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define NUM 1024
enum errorcode
{
    USAGE_ERROR = 1,
    SOCKET_ERROR,
    BIND_ERROR,
    CONNECT_ERROR
};

class tcpclient
{
public:
    //构造函数
    tcpclient(const std::string& ip, const uint16_t& port)
        :_ip(ip)
        ,_port(port)
        ,_sock(-1)
    {}

    void initclient()
    {
        //创建套接字,创建失败打印错误原因
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if(_sock == -1)
        {
            logmessage(FATAL, "create socket error");//socket失败属于最严重的错误
            exit(SOCKET_ERROR);//退出
        }
        logmessage(NORMAL, "create socket success:%d", _sock);//创建套接字成功,打印让用户观察到
        //客户端不需要显式绑定,该工作交给操作系统完成
    }

    //启动客户端进程
    void run()
    {
        struct sockaddr_in local;
        local.sin_family = AF_INET;//通信方式为网络通信
        local.sin_port = htons(_port);//将网络字节序的端口号填入
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//填充结构体

        //客户端连接服务器
        if(connect(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)//客户端与服务器进行连接
        {
            logmessage(FATAL, "connect error");//connect失败属于最严重的错误
        }
        else
        {
            std::string msg;
            while(1)
            {
                //向服务器发送数据
                printf("Please enter#");
                getline(std::cin, msg);
                write(_sock, msg.c_str(), msg.size());
                //接收服务器发回的数据
                char buffer[1024];
                int n = read(_sock, buffer, sizeof(buffer)-1);
                if(n > 0)
                {
                    buffer[n] = 0;
                    printf("server return$%s\n", buffer);
                }
                else
                {
                    //read返回0,证明服务端关闭,客户端也要关闭
                    break;
                }
            }
        }
    }

    //析构函数要释放不使用的文件描述符
    ~tcpclient()
    {
        if( _sock >= 0)
            close(_sock);
    }
private:
    int _sock;//套接字文件描述符
    std::string _ip;//服务器IP地址
    uint16_t _port;//服务器的端口号
};

client.hpp

#include"log.hpp"
#include"client.hpp"

using namespace std;

static void Usage(string proc)
{
    printf("\nUsage:\n\t%s server_ip server_port\n\n", proc.c_str());
}
int main(int argc, char* argv[])
{
    if(argc != 3)//如果没输入端口号和目的IP,argc保存的命令参数就不是三个,进程出错
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }

    uint16_t port = atoi(argv[2]);
    string ip = argv[1];
    
    unique_ptr p(new tcpclient(ip, port));

    p->initclient();
    p->run();

    return 0;
}

运行结果:

socket套接字——TCP协议_第13张图片

你可能感兴趣的:(Linux,tcp/ip,网络,网络协议,linux,c++,c语言)