【linux--->网络套接字编程】

一、套接字编程基础

1.端口号

网络通信不是目的,目的是信息的使用,所以最终信息是要交付给应用程序使用的.应用程序有很多个进程组成,所以网络通信其本质也是进程间通信,传输层位了识别数据包要传递给的应用层的目标进程,协议中包含了标识唯一进程的端口号,一个端口号只能被一个进程占用,但是一个进程可以绑定多个端口号;端口号是16位2字节的整数;ip地址加上端口号可以确定网络中唯一的网络进程.传输层协议中包含两个端口号,一个是源端口号,一个是目的端口号.表示那个进程发的数据包,发给谁的.

2.网络通信字节序

每个主机都已自己的字节序,要么是大端存储,要么是小端存储,但是在网络传输中必须是大端字节序,因为字节序不同会导致信息不正确,所以设备网络通信之前要将字节序转化为大端字节序以后再传输.
字节序转换接口介绍
h为host代表主机,n为network代表网络,l代表32位,s代表16位.

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

3.TCP/UDP协议

传输层的TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的传输协议。

TCP是一种可靠的、面向连接的协议。它提供了数据包的可靠传输,确保数据包的顺序和完整性。TCP使用三次握手建立连接,通过序列号和确认应答来保证数据传输的可靠性。TCP适用于需要可靠传输的应用,如网页浏览、文件传输和电子邮件等。

UDP是一种无连接的协议。它将数据分割成数据报,并直接发送到目标地址,不需要建立连接。UDP不提供可靠性和顺序保证,因此在传输过程中可能会有数据包丢失或乱序。UDP适用于对传输速度要求较高、数据丢失不会造成严重影响的应用,如实时音视频传输、在线游戏和DNS查询等。

选择使用TCP还是UDP取决于应用的需求。如果需要可靠性和顺序保证,则选择TCP;如果对实时性和传输速度要求较高,可以选择UDP。

二、套接字编程接口介绍

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同


// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

网络通信其本质就是进程间通信,用的是POSIX标准的进程间通信,POSIX标准的进程间通信既可以本地进程间通信,又可以网络进程间通信.网络通信是用ip地址和端口号来进行通信的,使用的是sockaddr_in通信地址结构体,而本地通信是用路径名来进行通信的,使用的是sockaddr_un通信地址结构体,这样不同通信方式就需要定义不同的通信接口,这将增加使用者使用负担,所以为了不冗余化接口,统一结构体地址传参.统一强制转换成sockaddr结构体指针传参,sockaddr指针指向一个包含16位地址类型和一个14字节的地址数据.sockaddr指针是形参,实参在传参是还是什么通信方式就传递什么结构体,然后再强制转换成sockaddr指针类型,接口内部再根据sockaddr头一个16位地址类型判断是网络通信,还是本地通信,再将结构体强制转换成该结构体类型使用.
sockaddr_in结构体中包含_SOCKADDR_COMMON(sin_),这本质上是一个宏,这个宏的作用是灵活匹配sa_family_t类型的变量名,宏展开后是sa_family_t sa_prefix##family,其中sa_family_t是一个用unsigned short int类型typedef的类型名,family是这个类型的原始类型,##用于连接宏参数灵活字段生成与family生成新的变量名.说白了就是改变变量名.而这个变量是用来标识通信方式是网络通信AF_INET或者是AF_UNIX本地通信的.
sockaddr结构体还包含sin _prot端口号和sin_addr IP地址,sin_zero填充字符成员变量;

struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;			/* Port number.  */
    struct in_addr sin_addr;		/* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
};
#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

socket接口是用来打开网络文件的,其中domain是用来标识是网络或者本地通信方式的参数,type参数用于指定套接字的类型,例如SOCK_STREAM表示流式套接字,SOCK_DGRAM表示数据报套接字等。而protocol参数则用于指定套接字使用的协议。
尽管套接字类型可以指定套接字的基本行为,但是在某些情况下,仅仅通过类型还无法完全确定套接字的行为。例如,对于流式套接字,可以使用TCP或UDP协议进行通信,而对于数据报套接字,也可以使用UDP或其他数据报协议进行通信。
在这种情况下,protocol参数就起到了一个进一步细化套接字行为的作用。它可以指定具体的协议,例如IPPROTO_TCP表示使用TCP协议,IPPROTO_UDP表示使用UDP协议,IPPROTO_ICMP表示使用ICMP协议等。
通过type和protocol两个参数的组合,可以灵活地创建不同类型和协议的套接字,满足不同的网络通信需求。

三、套接字编程实例

1.udp套接字编程

error头文件的作用是统一错误代码信息,汇集文件中所有错误信息代码.
error.hpp

#pragma once
#include<iostream>
using namespace std;
enum err
{
    SOCK_ERR=1,
    BIND_ERR,
    RECV_ERR,
    SENDTO_ERR,
};

mutex.hpp

#pragma once
#include<pthread.h>
using namespace std;
class lockGuard
{
public:
    lockGuard(pthread_mutex_t* mutex)
    :_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~lockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};

ringqueue.hpp

#pragma once
#include<iostream>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>
#include<vector>
using namespace std;
static const int n=5;
template <class T> 
class ringQueue
{
private:
    //p操作封装
    void p(sem_t& sem)
    {
        sem_wait(&sem);
    }
    //v操作封装
    void v(sem_t& sem)
    {
        sem_post(&sem);
    }
    //加锁封装
    void lock(pthread_mutex_t& mutex)
    {
        pthread_mutex_lock(&mutex);
    }
    //解锁封装
    void unlock(pthread_mutex_t& mutex)
    {
        pthread_mutex_unlock(&mutex);
    }
public:
    //构造
    ringQueue():_capacity(n),_c_step(0),_p_step(0),_container(_capacity)
    {
        //初始化空间和资源信号量
        sem_init(&_space_sem,0,_capacity);
        sem_init(&_data_sem,0,0);
        //初始化消费者和生产者互斥锁
        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);
    }
    //生产
    void push(const T& data)
    {
        //申请空间信号量
        p(_space_sem);
        //加锁
        lock(_p_mutex);
        //插入数据
        _container[_p_step++]=data;
        //将访问下标控制在环形区域内
        _p_step%=_capacity;
        //解锁
        unlock(_p_mutex);
        //释放资源信号量
        v(_data_sem);
    }
    //消费
    void pop(T* ret)
    {
        //申请资源信号量
        p(_data_sem);
        //加锁
        lock(_c_mutex);
        //将资源输出给消费者
        *ret=_container[_c_step++];
        //控制下标
        _c_step%=_capacity;
        //解锁
        unlock(_c_mutex);
        //释放空间信号量
        v(_space_sem);
    }
    //析构
    ~ringQueue()
    {
        //释放锁和信号量
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    //环形队列容量
    int _capacity;
    //消费者访问位置
    int _c_step;
    //生产者访问位置
    int _p_step;
    //容器
    vector<T> _container;
    //空间信号量
    sem_t _space_sem;
    //数据信号量
    sem_t _data_sem;
    //消费者锁
    pthread_mutex_t _c_mutex;
    //生产者锁
    pthread_mutex_t _p_mutex;
};

thread.hpp

#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include<functional>
using namespace std;

char *to_hexadecimal(int address)
{
    char *buffer = new char[64];
    snprintf(buffer, 64, "0x%X", address);
    return buffer;
}
class Thread
{
public:
    // 定义状态枚举
    typedef enum threadStatus
    {
        NEW,
        RUNNING,
        EXIT,
    } Status;
    // 重定义例程函数指针类型
    /* typedef void (*func_t)(void *); */
    using func_t =function<void(void)>;

public:
    // 构造,例程函数指针,例程参数
    Thread(func_t func)
        : _tid(0),
          _status(NEW),
          _func(func)
          /* _args(args) */
    {
        char name[64];
        snprintf(name, sizeof(name), "thread-%s", to_hexadecimal(pthread_self()));
        _name = name;
    }
    // 返回名字
    const string &name() const
    {
        return _name;
    }
    // 返回状态
    const Status &status() const
    {
        return _status;
    }
    // 返回id
    const pthread_t &tid() const
    {
        return _tid;
    }
    // 运行例程,设置状态
    //pthread_create接口的routine参数类型是返回值void*参数为void* 的函数指针
    //类成员函数中默认有一个隐藏参数*this,所以需要用进程成员函数
    static void* threadRoutine(void*args)
    {
        //静态成员函数没有this指针,所以无法直接调用类的私有成员属性,所以需要pthread_create传参this
        Thread* td=(Thread*)args;
        //利用()操作符重载调用例程
        (*td)();
    }
    void operator()()
    {
        //外部例程可以接收到主线程传递的例程参数
        _func();
    }
    void run()
    {
        int ret=pthread_create(&_tid, nullptr, threadRoutine, this);
        if(ret!=0)exit(1);
        _status=RUNNING;
    }
    // 等待例程
    void join()
    {
        int ret=pthread_join(_tid,nullptr);
        if(ret!=0)exit(1);
        _status=EXIT;
        //如果要使用例程的返回值,可以直接属性_args
    }
private:
    // 名字
    string _name;
    // id
    pthread_t _tid;
    // 状态
    Status _status;
    // 例程
    func_t _func;
/*     // 例程参数
    void *_args; */
};

群聊服务端:
1.接收用户传参的端口号,对端口号进行字符串转数字,然后将字节序转为网络字节序.
2.完成网络文件的创建与网络套接字结构体的初始化赋值,云服务器的ip地址不能绑定使用,本地主机可以,所以结构体的ip地址赋值INADDR_ANY(代表与主机所有服务器地址建立联系)
3.将网络文件与套接字结构体绑定.
4.建立两个线程,一个发送信息,一个接收信息.接收信息的同时将对端套接字信息加载到map容器中,将接收到的信息加载到消息队列,这样可以一边接受一边发送消息,发送信息之前,将信息进行处理,发送信息的时候遍历用户套接字信息,给所有用户发送信息.
5.两个线程都需要访问用户套接字信息容器,所以需要对用户容器加锁
6.线程在传参发送和接收信息的例程函数时,由于线程类的封装中,func的类型void(void*),所以要更改func的类型,又因为发送和接收两个例程是类的成员函数,有this隐藏参数,所以要用bind函数绑定器改变函数参数个数后再传参.
error.hpp

#pragma once
#include<iostream>
using namespace std;
enum 
{
    SOCK_ERR=1,
    BIND_ERR,
    RECV_ERR,
    SENDTO_ERR,
    LISTEN_ERR,
    READ_ERR,
    SETSID_ERR,
    OPEN_ERR,
};

log.hpp


#pragma once
#include<iostream>
#include<string>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdarg.h>

#define filename "tcpserver.log"

using namespace std;
enum 
{
    /* DEBUG:用于调试目的,提供详细的程序执行信息。
    INFO:提供程序执行的重要信息,用于跟踪程序的进程。
    WARN:表示潜在的问题,可能导致程序错误或异常。
    ERROR:表示程序中的错误,但不会导致程序终止。
    FATAL:表示严重的错误,可能导致程序终止。 */

    Debug,
    Info,
    Warn,
    Error,
    Fatal,
};

//将日志等级转换为字符串
static string get_leve_string(int leve)
{
    switch(leve)
    {
        case Debug:return "Debug";
        case Info:return "Info";
        case Warn:return "Warn";
        case Error:return "Error";
        case Fatal:return "Fatal";
        default: "Unkown";
    }
}

//获取系统当前时间
static string get_time()
{
    //获取时间戳
    time_t cur=time(nullptr);

    //转换日期结构
    struct tm* tmp=localtime(&cur);
    char buffer[1024];

    //格式化日期到buffer
    snprintf(buffer,sizeof(buffer),"%d-%d-%d_%d:%d:%d",
    tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,tmp->tm_hour,tmp->tm_min,tmp->tm_sec);

    return buffer;
}

//输出格式化日志信息
static void logMessage(int leve,const char* format,...)
{
    char logLeft[2048];
    char logRight[2048];
    //格式化时间,pid,日志等级
    string leve_string=get_leve_string(leve);
    string time_string=get_time();
    snprintf(logLeft,sizeof(logLeft),"[%s][%s][%d]",leve_string.c_str(),time_string.c_str(),getpid());

    //获取并格式化可变参数
    va_list p;
    va_start(p,format);
    vsnprintf(logRight,sizeof(logRight),format,p);
    va_end(p);

    //输出参数到文件
    FILE* fp=fopen(filename,"a");
    if(fp==nullptr)return ;
    fprintf(fp,"%s %s\n",logLeft,logRight);
    fclose(fp);
}

daemon.hpp

#pragma once
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"error.hpp"
#include"log.hpp"
using namespace std;

//守护进程
void daem()
{
    //忽略异常
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);

    //去组长化
    if(fork()>0)exit(0);

    //创建会话
    pid_t id=setsid();
    if((int)id==-1)
    {
        logMessage(Fatal,"setsid error,exit code %d",SETSID_ERR);
        exit(SETSID_ERR);
    }

    //处理0 1 2 问题
    int fd=open("/dev/null",O_RDWR);
    if(fd==-1)
    {
        logMessage(Fatal,"open error,exit code %d",OPEN_ERR);
        exit(OPEN_ERR);
    }
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
    close(fd);
}

server.hpp

#pragma once
#include <sys/types.h>
#include <unordered_map>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <errno.h>
#include <cstring>
#include "error.hpp"
#include "ringqueue.hpp"
#include "mutex.hpp"
#include "thread.hpp"
using namespace std;
class udp_server
{
public:
  udp_server(uint16_t port ,function<string(string)> f)
      : _port(port),_f(f)
  {
    pthread_mutex_init(&_mutex, nullptr);
    // 初始化线程
    _p = new Thread(bind(&udp_server::recv, this));
    _c = new Thread(bind(&udp_server::broadcast, this));
    
  }
  // 运行
  void start()
  {
    // 创建网络文件,并接受文件描述符
    _sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sock_fd < 0)
      cout << "socket error" << endl;

    // 创建套接字结构体,并初始化赋值
    struct sockaddr_in local;
    bzero(&local, sizeof local);
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = INADDR_ANY;
    local.sin_port = htons(_port);

    // bind网络文件与套接字结构体
    if (bind(_sock_fd, (struct sockaddr *)&local, sizeof(local)) < 0)
    {
      cout << "bind error" << endl;
      exit(BIND_ERR);
    }


    // 运行线程
    _p->run();
    cout<<"_p->run()"<<endl;
    _c->run();
    cout<<"_c->run()"<<endl;
  }
  void addUser(const string &name, const struct sockaddr_in &sock)
  {
    lockGuard ld(&_mutex);
    if (_user_message.find(name) == _user_message.end())
    {
      _user_message.insert(make_pair(name, sock));
    }
  }
  // 接收信息
  void recv()
  {
    char buffer[1024];
    while (true)
    {
      // 接收网路文件中的信息
      struct sockaddr_in client;
      bzero(&client, sizeof client);
      socklen_t len = sizeof(client);
      
      int n=recvfrom(_sock_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client, &len);
      if(n==-1)
      {
        cout << "recvfrom error" << endl;
        exit(RECV_ERR);
      }
      buffer[n] = 0;


      // 提取接收到的对端套接字信息
      string name = inet_ntoa(client.sin_addr);
      name += "-";
      name += to_string(client.sin_port);

      // 检查用户信息的是否存在用户容器,不是就插入,否则不作为
      addUser(name, client);
      // 将消息加入消息队列
      _q.push(buffer);
      cout<<name<<"#"<<buffer<<endl;
    }
  }
  // 发送信息
  void broadcast()
  {
    string recv;
    while (true)
    {
      // 提取消息队列信息
      _q.pop(&recv);
      vector<struct sockaddr_in> v;
      // 遍历用户信息容器,另存用户套接字信息
      {
        lockGuard ld(&_mutex);
        for (auto user : _user_message)
        {
          v.push_back(user.second);
        }
      }
      recv=_f(recv);
      // 遍历发送给所有用户消息到网络文件中
      for (auto e : v)
      {
        sendto(_sock_fd, recv.c_str(), strlen(recv.c_str()), 0, (struct sockaddr *)(&e), sizeof(e));
      }
    }
  }
  // 析构
  ~udp_server()
  {
    pthread_mutex_destroy(&_mutex);
    _p->join();
    _c->join();
    delete _p;
    delete _c;
  }

private:
  uint16_t _port;
  int _sock_fd;
  // 消息队列
  ringQueue<string> _q;
  // 用户信息容器
  unordered_map<string, struct sockaddr_in> _user_message;
  // 用户信息容器锁
  pthread_mutex_t _mutex;
  // 线程类
  Thread *_p;
  Thread *_c;
  // 信息处理函数
  function<string(string)> _f;
};

发送端服务器,可以将网络与本地程序分离,必要时可以给服务器封装类传参信息处理函数,将受到的信息进行处理后再发送给客户端.
server.cc

#include"tcp_server.hpp"
#include"daemon.hpp"
#include<cstdio>
string excutCommand(const string& command)
{
    FILE* pf=popen(command.c_str(),"r");
    string result;
    char buffer[1024];
    while(fgets(buffer,sizeof(buffer),pf)!=nullptr)
    {
        result+=buffer;
    }
    pclose(pf);
    return result;
}
int main(int argc,char* argv[])
{
    uint16_t server_port=atoi(argv[1]);
    tcp_server ts(server_port,excutCommand);

    //初始化
    ts.init();

    //守护进程
    daem();
    ts.start();
    return 0;
}

客户端不用手动绑定端口和ip地址,操作系统会自动绑定,因为客户端的应用程序是不固定的,且没有规律可寻,所以端口是操作系统随机指派的,必须由操作系统来定,ip地址也是操作系统自动绑定.所以只需要打开网络文件就可以发送信息了,但是服务器的地址和端口号要知道,所以可以在main函数启动时接收用户传参的端口号和ip地址,再将字符串转换成对应的套接字结构体成员类型,初始化套接字结构体.
client.cc

#include <iostream>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "error.hpp"
using namespace std;
int main(int argc, char *argv[])
{
    // 打开网络文件
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    // 解析要发送的ip和端口号
    // 创建对端套接字结构体,并初始化赋值
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    string message;
    char buffer[2048];
    while (true)
    {
        // 发送信息到网络文件
        cout << "client send# ";
        getline(cin, message);
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));

        // 接收文件到用户缓冲
        struct sockaddr_in recv_sock;
        socklen_t len = sizeof(recv_sock);
        int n=recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&recv_sock, &len);
        if(n==-1)
        {
            cout << "recvfrom err" << endl;
            exit(RECV_ERR);
        }
        buffer[n]=0;


        // 打印服务端消息
        cout << "server info# " << buffer << endl;
    }
    return 0;
}

2.tcp套接字编程

不同于udp协议通信,tcp协议服务器不仅需要创建套接字,绑定套接字与ip地址和端口号,还要给将套接字设置监听状态,设置监听后,如果有网络连接,可以通过accept获取链接,与对端建立新的套接字进行通信.需要注意的是,如果是单执行流的服务程序会造成只能有一个客户端连接的情况,需要用双执行流将信息处理和获取链接分开处理.
**方案1:**多进程,让子进程调用信息处理函数,父进程继续循环获取连接,需要解决的是子进程的退出需要父进程等待回收资源:有两个解决方案,一个是将子进程退出时发出的SIGCHLD新信号设置为忽略,一个是在子进程内部创建一个孙子进程,然后退出子进程,子进程会被父进程等待接收,这时孙子进程就是孤儿进程,会被操作系统领养,当程序结束时会自动释放资源.;另外子进程会继承父进程的文件描述符,这就需要通信之前关闭不需要的文件描述符.
**方案2:**多线程,因为是在类内部创建的例程,所以例程必须是静态的,静态成员函数只能使用静态变量,所以需要另外创建一个类,包含server类对象的指针以及ip地址和端口号还有文件描述符等信息,用于调用信息处理函数,线程同样需要在退出时回收资源,可以用在例程中去连接的方法解决.
方案3: 线程池,对于方案2,在客户端少,任务需求小的前提下是可行的,但是当客户端多,每链接一个客户端就要创建一个线程,当线程创建达到上限时,系统就会崩溃,线程池可以解决这个问题,线程池内的线程并发执行,竞争信息处理任务,但是有一点,客户端与服务器的交互是死循环,所以当信息处理任务将线程占满时,其他客户端的io任务会在消息队列中等待,暂时无法与服务器交互,直到有用户退出.

tcp_server.hpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
#include <functional>
#include <signal.h>
#include"error.hpp"
#include"log.hpp"
#include"Task.hpp"
#include"threadPool.hpp"
using namespace std;
class tcp_server
{
    //信息处理函数类型
    using func = function<string(string)>;

    /* //例程参数包
    class thread_data
    {
    public:
        thread_data(int fd,const string& serverip,uint16_t port,tcp_server* ts)
        :_fd(fd),_serverip(serverip),_port(port),_ts(ts)
        {}
    public:
        int _fd;
        string _serverip;
        uint16_t _port;
        tcp_server* _ts;
    }; */
public:
    tcp_server(uint16_t port, func f)
        : _port(port), _f(f)
    {}
    void init()
    {
        // 创建网络文件
        _sock = socket(AF_INET, SOCK_STREAM, 0);

        // 初始化套接字结构体
        struct sockaddr_in server;
        memset(&server, 0, sizeof server);
        server.sin_addr.s_addr = INADDR_ANY;
        server.sin_port = htons(_port);
        server.sin_family = AF_INET;

        // 绑定网络文件与套接字结构体
        if (bind(_sock, (struct sockaddr *)&server, sizeof(server)) == -1)
        {
            logMessage(Fatal,"exit code:%d,bing error",BIND_ERR);
            exit(BIND_ERR);
        }

        // 监听
        if (listen(_sock, 32) != 0)
        {
            logMessage(Fatal,"exit code:%d,listen error",LISTEN_ERR);
            exit(LISTEN_ERR);
        }
        logMessage(Info,"初始化成功");
    }
   /*  static void* routine(void* args)
    {
        pthread_detach(pthread_self());
        thread_data* tdata=static_cast<thread_data*>(args);
        tdata->_ts->service(tdata->_fd,to_string(tdata->_port)+tdata->_serverip);
    } */
    void start()
    {
        while (true)
        {
            // 忽略子进程的等待
            //signal(SIGCHLD, SIG_IGN);

            // 获取链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int fd = accept(_sock, (struct sockaddr *)&client, &len);
            if (fd < 0)
            {
                logMessage(Info,"尝试重新建立连接");
                continue;
            }
            string clientip= inet_ntoa(client.sin_addr);
            uint16_t clientport=ntohs(client.sin_port);
            logMessage(Info,"链接%s->%d成功",clientip.c_str(),clientport);


            //v3版本,线程池
            Task t(fd,clientip+to_string(_port),bind(&tcp_server::service,this,placeholders::_1,placeholders::_2));
            threadpool<Task>::getInstance()->push(t);


           /*  //v2版本,多线程通信.
            pthread_t td;
            //给例程传参一个包含文件描述符,ip和端口号信息以及tcp_server类对象指针的类
            thread_data* tdata=new thread_data(fd,clientip,clientport,this);
            pthread_create(&td,nullptr,routine,tdata);*/


            //v1版本-->多进程通信
            //子进程通信,父进程获取链接
            /* pid_t id = fork();
            if (id < 0)
            {
                cerr << "fork error" << endl;
                exit(-1);
            }
            else if (id == 0)
            {
                // 通信服务
                service(fd, clientip+to_string(_port));
                exit(0);
            } */


        }
    }
    // 通信
    void service(int fd, const string &name)
    {
        while (true)
        {
            // 接收消息
            char buffer[1024];
            int n = read(fd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                logMessage(Info,"%s# %s",name.c_str(),buffer);
            }
            else if (n == 0)
            {
                logMessage(Info,"quit");
                break;
            }
            else
            {
               logMessage(Error,",exit code:%d,read error",READ_ERR);
            }

            // 发送消息
            string result = _f(buffer);
            //当命令错误时,result为空,所以要加一个0.
            result+='\0';
            write(fd, result.c_str(), result.size());
        }
    }

private:
    uint16_t _port;
    int _sock;
    func _f;
};

tcp_server.cc

#include"tcp_server.hpp"
#include<cstdio>
string excutCommand(const string& command)
{
    FILE* pf=popen(command.c_str(),"r");
    string result;
    char buffer[1024];
    while(fgets(buffer,sizeof(buffer),pf)!=nullptr)
    {
        result+=buffer;
    }
    pclose(pf);
    return result;
}
int main(int argc,char* argv[])
{
    uint16_t server_port=atoi(argv[1]);
    tcp_server ts(server_port,excutCommand);
    ts.init();
    ts.start();
    return 0;
}

客户端同样不需要手动绑定套接字和ip地址以及端口号,由操作系统在申请链接的时候自动绑定,申请链接可以设定申请次数.客户端不用监听套接字,只需要与确定的服务器进行连接通信即可.
tcp_client.cc

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

using namespace std;
int main(int argc,char* argv[])
{
    //接收用户输入ip和端口号
    string server_ip=argv[1];
    uint16_t server_port=atoi(argv[2]);

    //打开网络文件
    int sock=socket(AF_INET,SOCK_STREAM,0);

    //申请链接
    struct sockaddr_in server;
    memset(&server,0,sizeof server);
    server.sin_addr.s_addr=inet_addr(server_ip.c_str());
    server.sin_family=AF_INET;
    server.sin_port=htons(server_port);
    int count=5;
    while(connect(sock,(struct sockaddr*)&server,sizeof server)!=0)
    {
        cout<<"尝试重新获取连接......"<<endl;
        if(--count==0)
        {
            break;
        }
    } 
    if(count==0)
    {
        cout<<"获取连接失败"<<endl; 
        exit(-1);
    }
    

    //通信
    while(true)
    {
        //发送消息
        cout<<"client sendto# ";
        string message;
        getline(cin,message);
        write(sock,message.c_str(),message.size());

        //接收消息
        char buffer[1024];
        int n=read(sock,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"server recv " <<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"quit"<<endl;
            exit(0);
        }
        else
        {
            cout<<"read error"<<endl;
            exit(-1);
        }
    }

    return 0;
}

四、日志

日志系统(Logging system)是一种用于记录应用程序运行时产生的信息的工具或机制。它可以用于跟踪应用程序的状态、调试应用程序、分析错误和异常,并提供运行时的监控和分析。

日志系统通常将应用程序的输出信息按照不同的级别进行分类,例如调试信息、提示信息、警告信息和错误信息等。开发人员可以根据需要选择记录哪些级别的信息。日志系统还可以将日志消息写入不同的目标,如控制台、文件、数据库或远程服务器。

通过使用日志系统,开发人员可以更方便地追踪应用程序的运行过程,排查问题和优化性能。它也是应用程序开发和维护中重要的工具之一。

以下日志代码适用于小型项目的使用,用到了可变参数,时间格式化转换等函数.
可变参数的使用有专门的宏函数配合使用,va_list类型创建一个指针,用于操作可变参数使用,va_start(va_list p,format)用于将指针指向可变参数第一个参数,va_end(va_list p)用于表示可变参数的结尾,标识为null.vsnprintf()函数用户获取可变参数到一个字符数组中.


#pragma once
#include<iostream>
#include<string>
#include<time.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdarg.h>

#define filename "tcpserver.log"

using namespace std;
enum 
{
    /* DEBUG:用于调试目的,提供详细的程序执行信息。
    INFO:提供程序执行的重要信息,用于跟踪程序的进程。
    WARN:表示潜在的问题,可能导致程序错误或异常。
    ERROR:表示程序中的错误,但不会导致程序终止。
    FATAL:表示严重的错误,可能导致程序终止。 */

    Debug,
    Info,
    Warn,
    Error,
    Fatal,
};

//将日志等级转换为字符串
static string get_leve_string(int leve)
{
    switch(leve)
    {
        case Debug:return "Debug";
        case Info:return "Info";
        case Warn:return "Warn";
        case Error:return "Error";
        case Fatal:return "Fatal";
        default: "Unkown";
    }
}

//获取系统当前时间
static string get_time()
{
    //获取时间戳
    time_t cur=time(nullptr);

    //转换日期结构
    struct tm* tmp=localtime(&cur);
    char buffer[1024];

    //格式化日期到buffer
    snprintf(buffer,sizeof(buffer),"%d-%d-%d_%d:%d:%d",
    tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,tmp->tm_hour,tmp->tm_min,tmp->tm_sec);

    return buffer;
}

//输出格式化日志信息
static void logMessage(int leve,const char* format,...)
{
    char logLeft[2048];
    char logRight[2048];
    //格式化时间,pid,日志等级
    string leve_string=get_leve_string(leve);
    string time_string=get_time();
    snprintf(logLeft,sizeof(logLeft),"[%s][%s][%d]",leve_string.c_str(),time_string.c_str(),getpid());

    //获取并格式化可变参数
    va_list p;
    va_start(p,format);
    vsnprintf(logRight,sizeof(logRight),format,p);
    va_end(p);

    //输出参数到文件
    FILE* fp=fopen(filename,"a");
    if(fp==nullptr)return ;
    fprintf(fp,"%s %s\n",logLeft,logRight);
    fclose(fp);
}

五、守护进程

1.会话

xshell每次登录都会创建一个会话,在Linux中,会话(session)是一个用户与操作系统进行交互的过程。它通常从用户登录开始,直到用户退出登录或关闭终端。一个会话可以包含多个进程,这些进程可以通过终端或其他方式与用户进行交互。bash在xshell中自成一个会话,会话包含进程组.

2.进程组

进程组是一个或多个进程的组合,主要用来合作完成任务,所以进程也叫做任务,每个组都有一个进程组ID,进程组的第一个进程时组长,组ID也是以组长的pid命名,每个组的会话ID都是一样的,因为他们同属一个会话.当会话被关闭时,会话中的进程都会被关闭,所以为了不影响服务器的运行,服务器也需要自成会话成为守护进程.
在这里插入图片描述

创建会话接口
pid_t setsid(void);
返回一个会话id,失败返回-1

调用setsid的进程不能是组长,所以在内部创建一个进程并关闭其父进程,就变成了孤儿进程在运行服务器.然后创建一个单独的会话.在这之前需要处理可能出现的不重要的异常信号引起的程序退出,将这些信号递达动作设置为忽略,然后将服务器代码中各种向0 1 2 文件中输入输出的情况处理,让这些信息打印到dev/null文件中去,dev/null文件相当于回收站 .
在这里插入图片描述
daemon.hpp

#pragma once
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"error.hpp"
#include"log.hpp"
using namespace std;

//守护进程
void daem()
{
    //忽略异常
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);

    //去组长化
    if(fork()>0)exit(0);

    //创建会话
    pid_t id=setsid();
    if((int)id==-1)
    {
        logMessage(Fatal,"setsid error,exit code %d",SETSID_ERR);
        exit(SETSID_ERR);
    }

    //处理0 1 2 问题
    int fd=open("/dev/null",O_RDWR);
    if(fd==-1)
    {
        logMessage(Fatal,"open error,exit code %d",OPEN_ERR);
        exit(OPEN_ERR);
    }
    dup2(fd,0);
    dup2(fd,1);
    dup2(fd,2);
    close(fd);
}

server.cc

#include"tcp_server.hpp"
#include"daemon.hpp"
#include<cstdio>
string excutCommand(const string& command)
{
    FILE* pf=popen(command.c_str(),"r");
    string result;
    char buffer[1024];
    while(fgets(buffer,sizeof(buffer),pf)!=nullptr)
    {
        result+=buffer;
    }
    pclose(pf);
    return result;
}
int main(int argc,char* argv[])
{
    uint16_t server_port=atoi(argv[1]);
    tcp_server ts(server_port,excutCommand);

    //初始化
    ts.init();

    //守护进程
    daem();
    ts.start();
    return 0;
}

3.进程组控制命令

任何时候在前台运行的进程都只能是一个,任何一个进程都有一个任务号,jobs命令查看后台进程
在这里插入图片描述
fg +任务号 命令可以将后台进程变为前台进程
在这里插入图片描述
ctrl+z可以将前台进程暂停运行到后台
在这里插入图片描述
bg+任务号可将暂停运行的后台进程恢复后台运行
在这里插入图片描述

六、握手和挥手

1.握手

三次握手是在TCP/IP协议中用于建立可靠的连接的过程。它确保了通信双方的同步和可靠性。

第一次握手:客户端向服务器发送一个带有SYN(同步)标志的数据包,表示请求建立连接。

第二次握手:服务器收到客户端的请求后,向客户端发送一个带有SYN/ACK(同步/确认)标志的数据包,表示连接已被接受。

第三次握手:客户端收到服务器的响应后,再次向服务器发送一个带有ACK(确认)标志的数据包,表示连接已建立。

通过这个三次握手的过程,客户端和服务器确认彼此的能力和意愿来建立连接,并且在连接建立之后,双方可以开始进行数据传输。如果在握手过程中出现问题,如某个数据包丢失或超时,TCP协议会重新发送数据包,直到建立成功或达到一定次数的尝试。

这种三次握手的方式可以防止已经失效的连接请求被服务器误认为是新的连接请求,从而保证了连接的可靠性和稳定性。

2.挥手

四次挥手是指在TCP连接关闭时,双方进行的一系列步骤,以确保数据的完整传输和连接的正常关闭。以下是四次挥手的步骤:

  1. 第一次挥手(FIN):客户端发送一个FIN报文给服务器,表示客户端没有数据要发送了,但仍然可以接收数据。

  2. 第二次挥手(ACK):服务器收到客户端的FIN报文后,发送一个ACK报文给客户端,确认收到了FIN报文。

  3. 第三次挥手(FIN):服务器发送一个FIN报文给客户端,表示服务器也没有数据要发送了。

  4. 第四次挥手(ACK):客户端收到服务器的FIN报文后,发送一个ACK报文给服务器,确认收到了FIN报文。

在这个过程中,每个报文都需要对方确认收到(ACK),以确保双方的数据传输和连接状态的同步。最终,当双方都发送了FIN报文并收到了对方的ACK报文后,TCP连接才会正式关闭。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值