慢吞云雾缓吐愁 发表于 2024-10-5 02:14:01

socket网络编程

本文只介绍基于IPv4的socket网络编程
端标语

https://i-blog.csdnimg.cn/direct/e686fd0cf54441cb87c244c9cfb6fd59.png
一个历程可以绑定多个端标语; 但是一个端标语不能被多个历程绑定
认识TCP协议

   此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识; 后面我们再具体讨论TCP的一些细节问题.


[*]传输层协议
[*]有连接
[*]可靠传输
[*]面向字节流
认识UDP协议 

   此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识, 后面再具体讨论.


[*]传输层协议
[*]无连接
[*]不可靠传输
[*]面向数据报
网络字节序 

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何界说网络数据流的地址呢?


[*]发送主机通常将发送缓冲区中的数据按内存地址从低到高的次序发出;
[*]吸取主机把从网络上接到的字节依次保存在吸取缓冲区中,也是按内存地址从低到高的次序保存;
[*]因此,网络数据流的地址应如许规定:先发出的数据是低地址,后发出的数据是高地址.
[*]TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
[*]不管这台主机是大端机照旧小端机, 都会按照这个TCP/IP规定的网络字节序来发送/吸取数据;
[*]如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
为使网络步伐具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
https://i-blog.csdnimg.cn/direct/acf0e23940584093b2ebf961109925ff.png


[*]这些函数名很好记,h表现host,n表现network,l表现32位长整数,s表现16位短整数
[*]比方htonl表现将32位的长整数从主机字节序转换为网络字节序,比方将IP地址转换后预备发送
[*]如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
[*]如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
套接字编程种类


[*]域间套接字编程---同一机器内
[*]原始套接字编程---网络工具
[*]网络套接字编程---用户间的网络通信
我们这里讲网络套接字编程
socket 常见API

https://i-blog.csdnimg.cn/direct/0110e27832c048148dab881c601fe542.png
socket API是一层抽象的网络编程接口,实用于各种底层网络协议,如IPv4、IPv6,然而, 各种网络协议的地址格式并不相同.
https://i-blog.csdnimg.cn/direct/adcaafb149684694893bd47f5f6e3aba.png
Pv4地址用sockaddr_in布局体表现,包括16位地址类型, 16位端标语和32位IP地址.
IPv4、IPv6地址类型分别界说为常数AF_INET、AF_INET6. 如许,只要取得某种sockaddr布局体的首地址,
不需要知道具体是哪种类型的sockaddr布局体,就可以根据地址类型字段确定布局体中的内容.
socket API可以都用struct sockaddr *类型表现, 在使用的时候需要欺压转化成sockaddr_in; 如许的好处是步伐的通用性, 可以吸取IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr布局体指针做为参数;
sockaddr 布局
https://i-blog.csdnimg.cn/direct/a69d38286f3d4059bff6d7394af8171b.png
sockaddr_in 布局 
https://i-blog.csdnimg.cn/direct/80825b768c8541c29c538d897910229a.png
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据布局是sockaddr_in; 这个布局里重要有三部分信息: 地址类型, 端标语, IP地址. 
in_addr布局
https://i-blog.csdnimg.cn/direct/61f36ada3c204646a0660e499a4d0216.png
socket编程接口 

socket    (TCP/UDP, 客户端 + 服务器)
创建套接字,返回值是网络文件形貌符 (TCP/UDP, 客户端 + 服务器)
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件形貌符
https://i-blog.csdnimg.cn/direct/046fe1f5496b4667b62426e427d7415a.png
参数:
domain: 创建套接字的域,我们这里填AF_INET表现使用ipv4的网络协议
https://i-blog.csdnimg.cn/direct/7025bc0434c2485ea2a512c40e0c7ca3.pngtype:套接字对应的类型,SOCK_STREAM字节流套接子,SOCK_DGRAM数据报套接字,就是TCP协议是面向字节流的,UDP协议是面向数据报的
https://i-blog.csdnimg.cn/direct/e73345a81db747ec8b2d38f690b52b33.png
protocol:协议类型,我们这里不用理,设为0先
返回值,就是网络文件形貌符
https://i-blog.csdnimg.cn/direct/8de1fb149a1f4a2eb6d76c075bc77378.png
bind    (TCP/UDP, 服务器)
绑定套接字和端标语 
https://i-blog.csdnimg.cn/direct/6e0c18623971455eb101211328d4d976.png
https://i-blog.csdnimg.cn/direct/1eeb73cdb5c9484dae234a7d3b8ad146.png
绑定的IP地址和端标语都在addr里,这个布局体需要我们自己设置,再传进去
   伪代码:
struct sockaddr_in
{
        sin_family;//创建套接字的域  AF_INET
        sin_port;//端标语
        sin_addr;//IP地址
};
我们的步伐中对addr参数是如许初始化的 
https://i-blog.csdnimg.cn/direct/84cea67813964dadb9c1075baa484cb9.png

[*]将整个布局体清零;
[*]设置地址类型为AF_INET;
[*]网络地址为INADDR_ANY(0), 这个宏表现当地的恣意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 如许设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
[*]端标语为SERV_PORT, 我们界说为9999 
recvfrom   (UDP,服务器+客户端)
吸取发给套接字的数据报信息
https://i-blog.csdnimg.cn/direct/25292b7601494b1b912736f048bc9f18.png
参数很好明白,flags设为0,后面的布局体是输出型参数,记载是谁发的 
send     (UDP,服务器和客户端)
向套接字发送数据报信息  
https://i-blog.csdnimg.cn/direct/dc4573c65b184fd39790d6361c6d8e0b.png
listen  (TCP,服务器)
listen()声明sockfd处于监听状态, 并且最多答应有backlog个客户端处于连接等候状态, 如果吸取到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节后面文章有
Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种一直在等候连接到来的状态,设置套接字为listen状态,如许就可以接受外来的连接
https://i-blog.csdnimg.cn/direct/e6cd959af49e4ca58320e18ddd186a43.png
https://i-blog.csdnimg.cn/direct/42f52f03d7f54b3490a50acfecdd8548.png
accept   (TCP,服务器)
吸取连接


[*]三次握手完成后, 服务器调用accept()接受连接
[*]如果服务器调用accept()时还没有客户端的连接请求,就阻塞等候直到有客户端连接上来;
[*]addr是一个传出参数,accept()返回时传出客户端的地址和端标语
[*]如果给addr 参数传NULL,表现不关心客户端的地址
[*]addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以制止缓冲区溢出问题, 传出的是客户端地址布局体的现实长度(有可能没有占满调用者提供的缓冲区)
[*]返回的是新的文件形貌符,TCP服务器就是通过这个形貌符来进行通信的,socket创建的文件形貌符用来listen的
https://i-blog.csdnimg.cn/direct/7cf54f8fca9d48088b98d04c9580b1e8.png
https://i-blog.csdnimg.cn/direct/d6ab8336a3b742d19479215e0b3c452b.png
connect   (TCP,客户端) 
建立连接


[*]客户端需要调用connect()连接服务器
[*]connect和bind的参数情势同等, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址
https://i-blog.csdnimg.cn/direct/b5371292e3444a399aa288322065e6ca.png
https://i-blog.csdnimg.cn/direct/2ac1a3971c3b42e88135374abda9ab23.png
地址转换函数

本文只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表现32位的IP 地址但是我们通常用点分十进制的字符串表现IP地址,以下函数可以在字符串表现 和in_addr表现之间转换
字符串转in_addr的函数
https://i-blog.csdnimg.cn/direct/62860731d3c84fa0b165eb93a2727825.png
 in_addr转字符串的函数https://i-blog.csdnimg.cn/direct/188d434df1fb49459170b7d0405ed946.png
关于inet_ntoa 

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是否需要调用者手动开释呢?
https://i-blog.csdnimg.cn/direct/5d4b2e0d73c54c03b105f1fb1d025081.png
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行开释.那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 拜见如下代码: 
https://i-blog.csdnimg.cn/direct/0e2ade9ab52d46149912496273c7216d.png
运行结果如下:https://i-blog.csdnimg.cn/direct/97ec78a9e066447c9bcb230c7c8fbb35.png因为inet_ntoa把结果放到自己内部的一个静态存储区, 如许第二次调用时的结果会覆盖掉上一次的结果.
思索: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
在APUE中, 明确提出inet_ntoa不是线程安全的函数,但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。在多线程情况下, 保举使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题
简单的UDP网络步伐

UDP(User Datagram Protocol 用户数据报协议):


[*]传输层协议
[*]无连接
[*]不可靠传输
[*]面向数据报
用到的socket编程接口:

[*]socket,创建套接字
[*]bind,绑定套接字
[*]recvfrom,发送数据报
[*]send,吸取数据报
一个关于IP地址

当你使用的是云服务器,udp服务端绑定公网IP时,克制绑定,因为我们看到的是虚拟的IP地址,一般绑定0.0.0.0的IP地址表现绑定恣意地址的IP地址,凡是发给我服务端的数据,都要根据端标语向上交付。我们也不发起绑定固定地址,当你的主机有多个网卡和IP地址时,假如绑定的是固定的IP,服务器接受数据是只会接受这个IP地址的数据,其他的就吸取不了
https://i-blog.csdnimg.cn/direct/a8e06882eb0249e696ca79120e316f22.png
一个关于端标语port

端标语不是恣意绑定的,:系统内定的端标语,一般都要有固定的应用层协议使用,好比http是80,https是443,mysql是3306,这个是破例。所以我们绑定端标语时,绑定1024+以上
   netstat -nlup   查察UDP步伐
https://i-blog.csdnimg.cn/direct/7b83828239ee4340b5f2443a95d08ebb.png
这个是我的Gitee,UDP网络步伐的代码都在里面
UDP网络步伐代码
实现的结果:一个简易的无界面的聊天服务器,每来一个人来访问,服务器就添加一个用户,服务器里的用户可以一起聊天,实现群聊
UDP服务端代码

编程思路:

[*]创建套接字
[*]绑定IP和端标语
[*]吸取消息,添加用户
[*]发送信息,向每一个添加的用户发消息
udpserver.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <string>
#include <unordered_map>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"

// typedef std::function<std::string(const std::string &, const std::string &, uint16_t)> func_t;

std::string defaultip = "0.0.0.0";
uint16_t defaultport = 8888;
const int size = 1024;

Log log;
enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
};

class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip) : ip_(ip), port_(port), isrunning(false)
    {
    }
    void Init()
    {
      // 1.创建udp socket
      //   Udp 的socket是全双工的,允许被同时读写的
      socketfd_ = socket(AF_INET, SOCK_DGRAM, 0);
      if (socketfd_ < 0)
      {
            log(Fatal, "create socket erro,socketfd:%d", socketfd_);
            exit(SOCKET_ERR);
      }
      log(Info, "socket create success, socketfd: %d", socketfd_);

      // 2.绑定socket
      struct sockaddr_in local;
      bzero(&local, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(port_);                  // 需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
      local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 1. string -> uint32_t 2. uint32_t必须是网络序列的
      // local.sin_addr.s_addr=htonl(INADDR_ANY);//或者这样

      if (bind(socketfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
      {
            log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
      }
      log(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }
    void ChectUser(const struct sockaddr_in &client, uint16_t clientport, const std::string &clientip)
    {
      auto it = online_user_.find(clientip);
      if (it == online_user_.end())
      {
            online_user_.insert({clientip, client});
            std::cout << "[" << clientip << ":" << clientport << "]" << "add to online user" << std::endl;
      }
    }

    void BroadCast(const std::string &info, uint16_t clientport, const std::string &clientip)
    {
      for (const auto &users : online_user_)
      {
            std::string massege = "[";
            massege += clientip;
            massege += ":";
            massege += to_string(clientport);
            massege += "]# ";
            massege += info;
            socklen_t len = sizeof(users.second);
            sendto(socketfd_, massege.c_str(), massege.size(), 0, (struct sockaddr *)&users.second, len);
      }
    }
    void Run()
    {
      isrunning = true;
      char buff;
      while (true)
      {
            memset(buff,0,size);
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(socketfd_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            ChectUser(client, clientport, clientip);

            std::string info = buff;
            BroadCast(info, clientport, clientip);
      }
    }
    ~UdpServer()
    {
    }

private:
    int socketfd_;   // 网络文件描述符
    std::string ip_; // 绑定的IP地址
    uint16_t port_;// 绑定的端口号
    bool isrunning;
    std::unordered_map<std::string, struct sockaddr_in> online_user_; // 存在线用户
};
main.cc    运行服务端
#include "udpserver.hpp"
#include <memory>
#include <cstdio>
#include <vector>
#include <functional>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port\n"
            << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
      Usage(argv);
      exit(0);
    }
    uint16_t port = std::stoi(argv);
    // std::unique_ptr<UdpServer> svr(new UdpServer(port));
    UdpServer *svr = new UdpServer(port);
    svr->Init();
    svr->Run();
    return 0;
} UDP客户端代码

udpclient.cc
编程思路:
1. 创建套接字
2. client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
 一个端标语只能被一个历程bind,对server是如此,对于client,也是如此!
其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以! 
系统什么时候给我bind呢?首次发送数据的时候
3.创建两个线程分别进行收发数据
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void Usage(const std::string proc)
{
    std::cout << "\n\tUsage: " << proc << " serverip serverport\n"
            << std::endl;
}

struct ThreadData
{
    std::string serverip;
    struct sockaddr_in server;
    int sockfd;
};

void *Recv_massege(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    char buff;
    while (true)
    {
      memset(buff, 0, sizeof(buff));
      struct sockaddr_in tmp;
      socklen_t len = sizeof(tmp);
      ssize_t n = recvfrom(td->sockfd, buff, sizeof(buff), 0, (struct sockaddr *)(&tmp), &len);
      if (n > 0)
      {
            buff = 0;
            std::cerr<< buff << std::endl;
      }
    }
}

void *Send_massege(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    std::string massege;
    socklen_t len = sizeof(td->server);

    while (true)
    {
      std::cout << "Please Enter@ ";
      getline(std::cin, massege);
      // 1. 数据 2. 给谁发
      sendto(td->sockfd, massege.c_str(), massege.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(0);
    }

    std::string serverip = argv;
    uint16_t serverport = std::stoi(argv);
    ThreadData td;
    bzero(&td.server, sizeof(td.server));

    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
      std::cout << "sock erro" << std::endl;
      exit(1);
    }
    td.serverip = serverip;

    // client 要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!
    // 系统什么时候给我bind呢?首次发送数据的时候

    // 创建两个线程,分别收发数据
    pthread_t recv_tid, send_tid;
    pthread_create(&recv_tid, nullptr, Recv_massege, &td);
    pthread_create(&send_tid, nullptr, Send_massege, &td);

    pthread_join(recv_tid, nullptr);
    pthread_join(send_tid, nullptr);

    return 0;
}

https://i-blog.csdnimg.cn/direct/02477a4fec1a4b858e9066d5d0a7fdd8.png
结果就是如许,大概就是实现一个群聊,大家都访问这个服务器,就可以一起聊天 
可能大家看不懂这里重定向
https://i-blog.csdnimg.cn/direct/a6cc9a7d5b9b41799fb1e0f11d7db5df.png
系统根目录下有个 /dev/pts/这个目录,这个目录里面是会话文件,你新起一个会话,就增长一个会话文件
https://i-blog.csdnimg.cn/direct/63a80788b1b14f9593e03bc96d75f2cc.png
你可以通过重定向字符串来知道哪个会话文件是哪个会话
https://i-blog.csdnimg.cn/direct/c0b63881a3c941498154a2143803efc2.png
守护历程

前台和背景历程

历程分为前台历程和背景历程,每有一个用户登录Linux系统时,都会生成一个会话,每一个会话都会有个前台历程,bash历程,这个历程为我们提供了下令行解释,一个会话只能有一个前台历程,多个背景历程
https://i-blog.csdnimg.cn/direct/5a6c3e17a2d149a6969c11ebb8aeb5bf.png当我们运行我们步伐在前台时,bash历程就会酿成背景,所以那时就不能进行下令行解释。前台历程和背景历程都可以向显示器打印,但只有前台历程拥有键盘文件进行尺度输入,历程在背景了,键盘信号也吸取不到了
背景历程相干指令:
   

[*]运行步伐+&:酿成背景历程
[*]jobs:查察背景历程
[*]fa+背景使命号:把背景酿成前台历程
[*]bg+背景使命号:把停止的背景历程启动,启动后照旧背景历程
https://i-blog.csdnimg.cn/direct/116d5a135c5a47c8b77f4fe81a81494b.png

Linux历程间关系 

https://i-blog.csdnimg.cn/direct/884ee72c925740d082260d33989d1286.png守护历程原理

假如运行有背景历程,把所以会话关闭,你再次登录时发现这个背景历程还在的,虽然这个背景历程还在,但是会受到用户登录和退出的影响
https://i-blog.csdnimg.cn/direct/b5d3bbcf8aea4686a2a4995b9cceb766.png如果你想让一个历程不想受任何用户登录和注销的影响,那就把历程酿成守护历程,守护历程是自成历程组自成会话的历程,怎么做呢?
函数 setsid
https://i-blog.csdnimg.cn/direct/f8c25204710f4f8988256ddd82e7751c.png
把历程自成历程组自成会话,但是历程组的组长不能setsid,只能组内其他历程,所以我们可以进行创建子历程,直接让父历程直接退出,让子历程setsid
#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = "")
{
    // 1. 忽略其他异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);

    // 2. 将自己变成独立的会话
    if (fork() > 0)
      exit(0);
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty())
      chdir(cwd.c_str());

    // 4. 标准输入,标准输出,标准错误重定向至/dev/null,这个文件相当于垃圾桶,把标准输入,标准输出,标准错误的信息往这个垃圾桶里丢弃
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0)
    {
      dup2(fd, 0);
      dup2(fd, 1);
      dup2(fd, 2);
      close(fd);
    }
} 函数 deamon 系统提供的守护历程化的接口
https://i-blog.csdnimg.cn/direct/6aad9f6c3f14466482fdd8052e7b6e5c.png 从man手册里可以知道他的两个参数就是上面我们自己写的功能
我们一般都是自己写,但照旧可以用系统提供的
简单的TCP网络步伐

   netstat -nltp  查察TCP步伐
https://i-blog.csdnimg.cn/direct/0fb78af2461c41929e35b0e5312d71ff.png
TCP通信是全双工的
TCP(Transmission Control Protocol 传输控制协议):


[*]传输层协议
[*]有连接
[*]可靠传输
[*]面向字节流
用到的socket编程接口:

[*]socket,创建套接字
[*]bind,绑定套接字
[*]listen,监听
[*]accept,吸取连接
[*]connect,发起连接
这个是我的Gitee,UDP网络步伐的代码都在里面
TCP网络步伐代码
实现的结果:服务器给用户提供翻译服务,并且守护历程化了
TCP服务端代码

编程思路:

[*]创建套接字
[*]绑定IP和端标语
[*]开始监听
[*]吸取连接
[*]根据新连接来通信---代码里提供了多种版本
下面是重要的代码文件,其余文件都在上面的Gitee里
tcpserver.hpp  服务端
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "task.hpp"
#include "log.hpp"
#include "threadpool.hpp"
#include "daemon.hpp"

extern Log log;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大

enum
{
    SocketErr = 1,
    BindErr,
    ListenErr,
};
class TcpServer;

class ThreadData
{
public:
    ThreadData(TcpServer *tser, int sockfd, const std::string &ip, const uint16_t &port) : tser_(tser), sockfd_(sockfd), clientip_(ip), clientport_(port)
    {
    }

public:
    int sockfd_;
    std::string clientip_;
    uint16_t clientport_;
    TcpServer *tser_;
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const std::string &ip = defaultip) : port_(port), ip_(ip)
    {
    }
    void Init()
    {
      // 1.创建socket
      listensock_ = socket(AF_INET, SOCK_STREAM, 0);
      if (listensock_ < 0)
      {
            log(Fatal, "socket err,errno: %d,errstring: %s", errno, strerror(errno));
            exit(SocketErr);
      }
      log(Info, "socket success,listensock: %d", listensock_);
      // 2.bind IP和端口号
      struct sockaddr_in local;
      memset(&local, 0, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(port_);
      inet_aton(ip_.c_str(), &(local.sin_addr));

      if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
      {
            log(Fatal, "bind err,errno: %d,errstring: %s", errno, strerror(errno));
            exit(BindErr);
      }
      log(Info, "bind success,listensock: %d", listensock_);
      // 3.开始监听
      // Tcp是面向连接的,服务器一般是比较“被动的”,服务器一直处于一种一直在等待连接到来的状态
      if (listen(listensock_, backlog) < 0)
      {
            log(Fatal, "listen err,errno: %d,errstring: %s", errno, strerror(errno));
            exit(ListenErr);
      }
      log(Info, "listen success,listensock: %d", listensock_);
    }

    static void *Routine(void *args)
    {
      ThreadData *td = static_cast<ThreadData *>(args);
      td->tser_->Service(td->sockfd_, td->clientip_, td->clientport_);
      delete td;
      return nullptr;
    }
    void Run()
    {
      Daemon();                                 // 守护进程化
      signal(SIGPIPE, SIG_IGN);               // 把SIGPIPE忽略,防止服务器当读端关闭或者文件描述符关闭时收到这个信号导致服务器关闭
      ThreadPool<Task>::GetInstance()->Start(); // version4 启动线程池
      log(Info, "tcpserver is running");
      while (true)
      {
            // 1.获取新连接
            // signal(SIGCHLD,SIG_IGN);//下面多进程版本的进程等待,可以把SIGCHLD信号忽略,就不用写waitpid了
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_, (struct sockaddr *)&client, &len); // 这个sockfd是用来发送和接收消息的
            if (sockfd < 0)
            {
                log(Warning, "accept err,errno: %d,errstring: %s", errno, strerror(errno));
                continue;
            }
            uint16_t clientport = client.sin_port;
            char clientip;
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));

            // 2.根据新连接来通信
            log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);

            // version1 单进程版---缺陷:当有多个用户访问时,其他用户会被阻塞
            //Service(sockfd, clientip, clientport);
            //close(sockfd);

            // version2 多进程版---缺陷:创建进程耗资源,成本高
            //pid_t id=fork();
            //if(id==0)
            //{
            //      //child
            //      close(listensock_);
            //      if(fork()>0) exit(0);
            //      Service(sockfd, clientip, clientport);//孙子进程,由1号进程领养
            //      close(sockfd);
            //      exit(0);
            //}
            //close(sockfd);
            //pid_t rid=waitpid(id,nullptr,0);

            // version3 多线程版---缺陷:每来一个用户都要为它创建线程,会导致线程有很多,也是不太有利的
            //ThreadData* td=new ThreadData(this,sockfd,clientip,clientport);
            //pthread_t tid;
            //pthread_create(&tid,nullptr,Routine,td);

            // version4 线程池版本---最优
            Task t(sockfd, clientip, clientport);
            // ThreadPool<Task>::GetInstance()->Start();//在最开始启动不是在这
            ThreadPool<Task>::GetInstance()->Push(t);
      }
    }
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport)
    {
      while (true)
      {
            char buff;
            int n = read(sockfd, buff, sizeof(buff));
            if (n > 0)
            {
                buff = 0;
                std::cout << "client say@ " << buff << std::endl;
                std::string massege = "tcpserver say@ ";
                massege += buff;
                write(sockfd, massege.c_str(), massege.size());
            }
            else if (n == 0)
            {
                log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
      }
    }
    ~TcpServer()
    {
    }

private:
    int listensock_;
    uint16_t port_;
    std::string ip_;
}; main.cc 运行服务端
#include <iostream>
#include <memory>
#include "tcpserver.hpp"


void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port\n" << std::endl;
}
//./tcpserver 8080
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
      Usage(argv);
      exit(1);
    }
    const uint16_t port=std::stoi(argv);
    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->Init();
    tcp_svr->Run();
    return 0;
} TCP客户端代码 

编程思路:

[*]创建套接字
[*] tcp客户端要不要bind?要。要不要显示的bind?不要。系统进行bind,随机端口。客户端发起connect的时候,进行自动随机bind
[*] 发起连接
[*] 实现假如服务器突然断开,重连的现象
tcpclient.cc 客户端
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << "serverip serverport" << std::endl;
}
//./tcpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
      Usage(argv);
      exit(1);
    }
    const std::string serverip = argv;
    const uint16_t serverport = std::stoi(argv);
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);
    socklen_t len = sizeof(server);

    while (true)
    {
      // 创建socket
      int sockfd = socket(AF_INET, SOCK_STREAM, 0);
      if (sockfd < 0)
      {
            std::cerr << "socket err" << std::endl;
            exit(1);
      }
      //实现重连
      int cnt = 30;
      bool isreconnect = false;
      do
      {
            // tcp客户端要不要bind?1 要不要显示的bind?0 系统进行bind,随机端口
            // 客户端发起connect的时候,进行自动随机bind
            int n = connect(sockfd, (struct sockaddr *)&server, len);
            if (n < 0)
            {
                isreconnect = true;
                std::cerr << "connect err,isreconnect... cnt:" << cnt << std::endl;
                cnt--;
                sleep(1);
            }
            else
            {
                break;
            }
      } while (cnt && isreconnect);

      if (cnt == 0)
      {
            std::cerr << "user offline..." << std::endl;
            break;
      }
      std::string massege;
      std::cout << "Please Enter@ ";
      std::getline(std::cin, massege);
      int n = write(sockfd, massege.c_str(), massege.size());
      if (n < 0)
      {
            std::cerr << "write err" << std::endl;
      }

      char buff;
      n = read(sockfd, buff, sizeof(buff));
      if (n > 0)
      {
            buff = 0;
            std::cout << buff << std::endl;
      }
      close(sockfd);
    }

    return 0;
}
https://i-blog.csdnimg.cn/direct/8758dc4f80a443359fec1741a842cb82.png
TCP协议通讯流程

下图是基于TCP协议的客户端/服务器步伐的一般流程:
https://i-blog.csdnimg.cn/direct/f025a0fbe76d450a87074ea1f5e6060c.png
服务器初始化: 


[*]调用socket, 创建文件形貌符
[*]调用bind, 将当前的文件形貌符和ip/port绑定在一起; 如果这个端口已经被其他历程占用了, 就会bind失败
[*]调用listen, 声明当前这个文件形貌符作为一个服务器的文件形貌符, 为后面的accept做好预备
[*]调用accecpt, 并阻塞, 等候客户端连接过来
建立连接的过程:


[*]调用socket, 创建文件形貌符
[*]调用connect, 向服务器发起连接请求
[*]connect会发出SYN段并阻塞等候服务器应答(第一次)
[*]服务器收到客户端的SYN, 会应答一个SYN-ACK段表现"同意建立连接" (第二次)
[*]客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段 (第三次)
这个建立连接的过程, 通常称为三次握手
数据传输的过程:


[*]建立连接后,TCP协议提供全双工的通信服务; 所谓全双工的意思是, 在同一条连接中, 同一时候, 通信两边可以同时写数据; 相对的概念叫做半双工, 同一条连接在同一时候, 只能由一方来写数据
[*]服务器从accept()返回后立刻调 用read(), 读socket就像读管道一样, 如果没有数据到达就阻塞等候
[*]这时客户端调用write()发送请求给服务器, 服务器收到后从read()返回,对客户端的请求进行处理, 在此期间客户端调用read()阻塞等候服务器的应答
[*]服务器调用write()将处理结果发回给客户端, 再次调用read()阻塞等候下一条请求
[*]客户端收到后从read()返回, 发送下一条请求,如此循环下去
断开连接的过程:


[*]如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次)
[*]此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次)
[*]read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)
[*]客户端收到FIN, 再返回一个ACK给服务器 (第四次)
这个断开连接的过程, 通常称为四次挥手
TCP 和 UDP 对比



[*]可靠传输 vs 不可靠传输
[*]有连接   vs 无连接
[*]字节流   vs 数据报

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: socket网络编程