网络编程套接字

news/2024/5/18 16:37:19 标签: 网络, tcp/ip, udp

文章目录

  • 1. socket编程接口
    • 1-1 socket 常见API
    • 1-2 sockaddr结构
  • 2. 简单的UDP网络程序
    • 2-1 日志(固定用法:标准部分+自定义部分)
    • 2-2 服务器代码实现
      • 1. 框架
      • 2. 初始化服务器
      • 3. 服务器运行
      • 4. 调用服务器封装函数(UdpServer)
    • 2-3 客户端代码
    • 2-4 结果展示
    • 2-5 改进服务器
    • 2-6 改进用户
      • 1. 封装线程
      • 2. 创建两个线程(一个接受信息一个发送信息)
    • 2-7 改进后群聊功能展示
    • 2-8 总代码链接
  • 3. 简单的TCP网络程序
    • 3-1 日志(固定用法参考上面2.1)
    • 3-2 服务器代码实现
      • 1. 框架
      • 2. 初始化服务器
      • 3. 调用服务器封装函数(TcpServer)
      • 4. 服务器运行(铺垫)
      • 5. 服务器运行(多进程版)
      • 6. 服务器运行(多线程版)
    • 3-3 客户端代码
    • 3-4 结果展示
    • 3-5 总代码链接

1. socket编程接口

1-1 socket 常见API

// 创建 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);

1-2 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX Domain
Socket.

各种网络协议的地址格式并不相同;为了让接口统一我们根据前16位作区别(就好像原始模板划分为两个不同类型模块)

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用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 结构

/* Structure describing a generic socket address.  */
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in 结构

/* Structure describing an Internet socket address.  */
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)];
  };
  • 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。

in_addr结构

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };
  • in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。

2. 简单的UDP网络程序

2-1 日志(固定用法:标准部分+自定义部分)

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

using std::cout;
using std::endl;
using std::string;

// 日志是有日志级别的
#define DEBUG 0   //  调试
#define NORMAL 1  //  正常
#define WARNING 2 //  警告
#define ERROR 3   //  错误
#define FATAL 4   //  致命错误

static const size_t BUFFER_NUM = 1024;
const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"};

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void LogMessage(int level, const char *format, ...)	//	可变参数模板
{
    char stdBuffer[BUFFER_NUM]; // 标准部分
    const time_t timestamp = time(nullptr);
    struct tm *L_Time = localtime(&timestamp);
    string time;
    time += "日期-时间:" + std::to_string(L_Time->tm_year+1900) + "/" + std::to_string(L_Time->tm_mon) + "/" + std::to_string(L_Time->tm_mday) + "-" + std::to_string(L_Time->tm_hour) + ":" + std::to_string(L_Time->tm_min) + ":" + std::to_string(L_Time->tm_sec);
    std::to_string(L_Time->tm_sec);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s][%s]",
             gLevelMap[level], time.c_str());

    char logBuffer[BUFFER_NUM]; // 自定义部分
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);

    printf("%s%s\n", stdBuffer, logBuffer);
}

2-2 服务器代码实现

1. 框架

#ifndef _UDP_SERVERHPP
#define _UDP_SERVERHPP

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include "Log.hpp"

using std::cout;
using std::endl;
using std::string;

static const size_t SIZE = 1024;
class UdpServer
{
public:
    UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip)
    {
    }
    //  初始化
    bool InitServer()
    {
    }
    //  服务器运行
    void start()
    {
    }
    ~UdpServer()
    {
        if(_sock>0) close(_sock);   //  通过文件描述符关闭文件
    }

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    string _ip;

    int _sock = -1; // 创建 socket 文件描述符 的返回值
};

#endif

2. 初始化服务器

    1. 创建套接字
      在这里插入图片描述

固定用法AF_INET 和 SOCK_DGRAM;最后一个参数(设置成0)

    1. bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);1)参数 sockfd ,需要绑定的socket。(文件描述符)
  (2)参数 addr ,存放了服务端用于通信的地址和端口。
  (3)参数 addrlen ,表示 addr 结构体的大小
  (4)返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中。如果绑定的地址错误,或者端口已被占用,bind 函数一定会报错,否则一般不会返回错误。

注意:

我们普通用户见得最多的是这种"192.168.110.132"IP地址是点分十进制字符串风格的IP地址每一个区域取值范围是[0-255]: 1字节 -> 4个区域

理论上,表示一个IP地址,其实4字节就够了(减少浪费)
所有在网络上我们需要把点分十进制字符串风格的IP地址 转为4字节再转为网络序列
不过有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据(inet_addr)

用这个INADDR_ANY宏;表示bind任意IP(便于服务器接收数据)

    //  初始化
    bool InitServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        // 1. 完成套接字创建
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0) //  创建失败
        {
            LogMessage(FATAL, "%d:%s | %s | %d", errno, strerror(errno), __FILE__, __LINE__);
            exit(2);
        }
        // 2. 绑定(将用户设置的ip和port在内核中和我们当前的进程强关联)
        struct sockaddr_in local;
        bzero(&local, sizeof local);   //  结构体清理(初始化)
        local.sin_family = AF_INET;    //    协议家族
        local.sin_port = htons(_port); //  网络字节序大端存储
        //  将点分十进制字符串风格的IP地址 ->  4字节-> 网络序列
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_sock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            //  绑定失败
            LogMessage(FATAL, "%d:%s | %s | %d", errno, strerror(errno), __FILE__, __LINE__);
            exit(3);
        }
        LogMessage(NORMAL, "init udp server done ... | %s | %d", __FILE__, __LINE__);

        return true;
    }

3. 服务器运行

recvfrom () 用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socklen_t *fromlen);1)ssize_t 相当于 long int,socklen_t 相当于int2)sockfd:标识一个已连接套接口的描述字。
  (3)buf:接收数据缓冲区。
  (4)len:缓冲区长度。
  (5)flags:调用操作方式。是以下一个或者多个标志的组合体,可通过“ | ”操作符连在一起(通常设置为0)
  (6)from 是一个指向sockaddr结构体类型的指针;
  (7*fromlen表示my_addr结构的长度,可以用sizeof操作符获得。
  (8)返回值:成功则返回接收到的字符数,失败返回-1.
sendto() 用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作。参数msg指向欲连线的数据内容,参数flags 一般设0
int sendto ( socket s , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen ) ;1)s:一个用于标识已连接套接口的描述字。
  (2)buf:包含待发送数据的缓冲区。
  (3)len:缓冲区中数据的长度。
  (4)flags:调用执行方式。
  (5)to 是一个指向sockaddr结构体类型的指针;
  (5)参数tolen表示to结构的长度,可以用sizeof操作符获得。
  (6)成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
  • 代码块:
//  服务器运行
    void start()
    {
        // 作为一款网络服务器,永远不退出的!
        // 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        char buffer[SIZE]; //  存储读取到的数据
        for (;;)
        {
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof peer);
            socklen_t len = sizeof peer;
            //  读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0) //   读取成功
            {
                //  谁发的数据
                buffer[s] = 0;                           // 我们目前数据当做字符串
                uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的
                // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                string cliIP = inet_ntoa(peer.sin_addr);
                printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);
            }
            // end. 写回数据
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
        }
    }

4. 调用服务器封装函数(UdpServer)

#include "udp_server.hpp"
#include <memory>

static void Usage(string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}
// ./udp_server 127.0.0.1 8080
int main(int argc, char *argv[]) //  命令行参数
{
    if (argc != 3)
    {
        Usage(argv[0]); // 使用手册
        exit(1);
    }
    std::string ip = argv[1];
    u_int16_t port=atoi(argv[2]);
    std::unique_ptr<UdpServer> server(new UdpServer(port, ip));   //  智能指针

    server->InitServer();
    server->start();

    return 0;
}

2-3 客户端代码

  • 注意:
1)client要bind,但是一般client不会显示的bind,程序员不会自己bind
(2)client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port; 就芭比Q了
(3)client一般不需要显示的bind指定port,而是让OS自动随机选择(具体在当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT就是调用sendto()函数自动bind)
#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>

using std::cout;
using std::endl;
using std::string;

static const size_t SIZE = 1024;
static void Usage(string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "client: socket error" << endl;
        exit(2);
    }
    // client要要,但是一般client不会显示的bind,程序员不会自己bind
    string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    char buffer[SIZE];
    while (true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);

        struct sockaddr_in temp;
        socklen_t len = sizeof temp;
        memset(&temp, 0, len);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "server echo# " << buffer << endl;
        }
        std::cerr << "client: srecvfrom error" << endl;
    }
    close(sock);
    return 0;
}

2-4 结果展示

在这里插入图片描述

2-5 改进服务器

  • 我们不直接绑定服务器;达到如下效果
    在这里插入图片描述
  • 我们需要哈希桶完成映射;并存储用户信息
//  服务器运行
    void start()
    {
        // 作为一款网络服务器,永远不退出的!
        // 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        char buffer[SIZE]; //  存储读取到的数据
        for (;;)
        {
            char key[64];
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof peer);
            socklen_t len = sizeof peer;
            //  读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0) //   读取成功
            {
                //  谁发的数据
                buffer[s] = 0;                           // 我们目前数据当做字符串
                uint16_t cliPort = ntohs(peer.sin_port); // 从网络中来的
                // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                string cliIP = inet_ntoa(peer.sin_addr);
                // printf("[%s:%d]# %s\n", cliIP.c_str(), cliPort, buffer);
                snprintf(key, sizeof(key), "%s-%u", cliIP.c_str(), cliPort); // 127.0.0.1-8080
                LogMessage(NORMAL, "key: %s | %s | %d", key, __FILE__, __LINE__);

                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // 储存
                    LogMessage(NORMAL, "add new user : %s | %s | %d", key, __FILE__, __LINE__);
                    _users[key] = peer;
                }
            }
            // end. 写回数据
            for (auto &iter : _users)
            {
                string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                LogMessage(NORMAL, "push message to %s | %s | %d", iter.first.c_str(), __FILE__, __LINE__);
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }

在这里插入图片描述

2-6 改进用户

多线程跑起来;实现群聊功能

1. 封装线程

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstdio>

using std::cout;
using std::endl;
using std::string;

static const size_t NAME_NUM = 1024;
typedef void*(*fun_t)(void*);     // 线程执行的方法

class ThreadDate
{
public:
    void *_args;
    string _name;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void *args) : _func(callback)
    {
        char nameBuffer[NAME_NUM];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);

        _tdata._name = nameBuffer;
        _tdata._args = args;
    }
    void start() //  创造线程
    {
        pthread_create(&_tid, nullptr, _func, (void *)&_tdata);
    }
    void join() //  等待线程
    {
        pthread_join(_tid, nullptr);
    }
    string &name() //  线程名字
    {
        return _tdata._name;
    }

private:
    fun_t _func;
    ThreadDate _tdata;
    pthread_t _tid;
};

2. 创建两个线程(一个接受信息一个发送信息)

  • 其实稍微把发送和接受解耦;封装一下就可以了
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "thread.hpp"

using std::cout;
using std::endl;
using std::string;

uint16_t serverPort = 0;
string serverIp;
static const size_t SIZE = 1024;
static void Usage(string proc)
{
    cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << endl;
}

static void *UdpSend(void *args) //  发送消息
{
    ThreadDate *td = (ThreadDate *)args;
    int sock = *(int *)td->_args;
    string name = td->_name;

    // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
    string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());

    while (true)
    {
        std::cerr << "请输入你的信息# ";
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

static void *UdpAccept(void *args) //  接受信息
{
    ThreadDate *td = (ThreadDate *)args;
    int sock = *(int *)td->_args;
    string name = td->_name;

    char buffer[SIZE];
    while (true)
    {

        struct sockaddr_in temp;
        socklen_t len = sizeof temp;
        memset(&temp, 0, len);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
        else
        {
            std::cerr << "client: srecvfrom error" << endl;
        }
    }

    return nullptr;
}

// ./udp_client 127.0.0.1 8080
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0)
    {
        std::cerr << "client: socket error" << endl;
        exit(2);
    }

    serverIp = argv[1];
    serverPort = atoi(argv[2]);

    std::unique_ptr<Thread> Sender(new Thread(1, UdpSend, (void *)&sock));
    std::unique_ptr<Thread> Accepter(new Thread(2, UdpAccept, (void *)&sock));
    //  创建线程
    Sender->start();
    Accepter->start();
    //  等待进程
    Sender->join();
    Accepter->join();

    close(sock); //  关闭文件(sock本质是一个文件)

    return 0;
}

2-7 改进后群聊功能展示

注意:

  • 首次发消息;也再做管理该用户
    在这里插入图片描述

2-8 总代码链接

链接:
https://gitee.com/ding-xushengyun/linux__cpp/commit/6bfc59b29aae02abecb88d43b1f2b7b4c071fe2c

3. 简单的TCP网络程序

3-1 日志(固定用法参考上面2.1)

3-2 服务器代码实现

1. 框架

  • 框架跟UDP几乎相同
#pragma once

#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"


using std::cout;
using std::endl;
using std::string;

static const size_t SIZE = 1024;
class UdpServer
{
    const static int gbacklog = 20;
public:
    UdpServer(uint16_t port, string ip = "") : _port(port), _ip(ip)
    {
    }
    //  初始化
    bool InitServer()
    {
    }
    //  服务器运行
    void start()
    {
    }
    ~UdpServer()
    {
    }

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    string _ip;

    int _listenSock = -1; //  监听套接字(建立新链接---客户)
};

2. 初始化服务器

  • 初始化:1. 创建套接字(跟上面UDP几乎相同;不同的是第二参数UDP是数据报,我们是面向字节流) 2. 绑定bind;最大不同TCP只不过多了一个聆听- - -listen
  • 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接(就好像厨师一直在等待顾客的到来;因为我们不知道顾客什么适合来吃饭)
  • 云服务器不能绑定公有IP;需要随机绑定INADDR_ANY
int listen(int sockfd, int backlog);1)sockfd 参数表示监听的 socket 句柄
(2)backlog 参数表示接收请求队列的长度(不能太大)
(3)成功则返回 0,失败返回-1,错误原因存于errno 中。
所谓被动监听,是指当没有客户端请求时,套接字处于“睡眠”状态,只有当接收到客户端请求时,套接字才会被“唤醒”来响应请求。
  • 代码块:
    void InitServer()
    {
        //  1. 创建套接字
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0) //  创建失败
        {
            LogMessage(FATAL, "创建套接字失败 %d:%s", errno, strerror(errno));
            exit(2);
        }
        LogMessage(FATAL, "创建套接字成功, listensock: %d", _listenSock); //  3

        //  2. bind 绑定 文件+网络
        struct sockaddr_in local;
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_listenSock, (struct sockaddr *)&local, sizeof local) < 0)
        {
            LogMessage(FATAL, "绑定失败, %d:%s", errno, strerror(errno));
            exit(3);
        }

        // 3. 因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
        if (listen(_listenSock, gbacklog) < 0)
        {
            LogMessage(ERROR, "建立链接失败, %d:%s", errno, strerror(errno));
            exit(4);
        }

        LogMessage(NORMAL, "初始化成功!!!");
    }

3. 调用服务器封装函数(TcpServer)

  • 跟UDP没什么不同
#include <memory>
#include "tcp_server.hpp"

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

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]); // 使用手册
        exit(1);
    }
    
    uint16_t port=atoi(argv[1]);
    std::unique_ptr<TcpServer> sve(new TcpServer(port));

    sve->InitServer();
    sve->start();
    
    return 0;
}

4. 服务器运行(铺垫)

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);1)sock 为服务器端套接字
(2)addr 为 sockaddr_in 结构体变量
(3)addrlen 为参数 addr 的长度,可由 sizeof() 求得。
(4)成功则返回 socket(这个sock用来提供服务的),失败返回-1,错误原因存于errno 中。
tcp 面向字节流而且sock还是文件描述符;我们可以把接受和发送数据当作文件处理(read和write)
static void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
    char buffer[SIZE];
    for (;;)
    {
        // read && write 可以直接被使用
        ssize_t s = read(sock, buffer, sizeof buffer - 1);
        if (s > 0)
        {
            buffer[s] = 0; // 将发过来的数据当做字符串
            cout << clientip << ":" << clientport << "# " << buffer << endl;
        }
        else if (s == 0) // 对写端关闭连接
        {
            LogMessage(NORMAL, "%s:%d shutdown, me too!", clientip.c_str(), clientport);
            break;
        }
        else
        { //
            LogMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }

        write(sock, buffer, strlen(buffer));
    }
}
  • 我们链接(accept)成功后;直接调用service()函数我们得到的只是一个单进程版;什么意思呐?如下图:
    在这里插入图片描述
    (用telnet简单测试一下服务器)我们明显发现只能一个客户发消息;其它客户发消息需要等第一个客户退出才能行。

5. 服务器运行(多进程版)

怎么实现服务器并发处理客户端呐?

  • 我们需要让子进程提供服务(调用service函数);父进程监听socket
  • 子进程继承父进程的页表;父进程和子进程需要关闭相应文件(子进程:close(_listenSock); 父进程: close(serviceSock);)
  • 子进程退出会产生僵尸问题(我们需要父进程不阻塞式处理)
    • a. 信号捕捉;主动忽略(signal(SIGCHLD, SIG_IGN);)
    • b. 子进程再fork() ,子进程退出;子进程的子进程执行服务(它退出后形成孤儿进程被Init 1一号进程领养)

我们采用信号捕捉的方法

    void start()
    {
        // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态
        signal(SIGCHLD, SIG_IGN);
        for (;;)
        {
            // 4. 获取连接
            struct sockaddr_in src;
            memset(&src, 0, sizeof src);
            socklen_t len = sizeof src;
            //  获取新连接
            int serviceSock = accept(_listenSock, (struct sockaddr *)&src, &len);
            if (serviceSock < 0)
            {
                LogMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
                continue;
            }

            // 获取连接成功了
            uint16_t clientPort = ntohs(src.sin_port);
            string clientIp = inet_ntoa(src.sin_addr);
            LogMessage(NORMAL, "link success, servicesock: %d | %s : %d |\n",
                       serviceSock, clientIp.c_str(), clientPort);

            // 开始进行通信服务啦
            // version 1 -- 单进程循环版 -- 只能够进行一次处理一个客户端,处理完了一个,才能处理下一个
            // service(serviceSock, clientIp, clientPort);

            // version 2.0 -- 多进程版 --- 创建子进程
            // 让子进程给新的连接提供服务,子进程能打开父进程曾经打开的文件fd 1 0
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                //  子进程:提供服务    不需要监听socket
                close(_listenSock);

                service(serviceSock, clientIp, clientPort);
                exit(0); //  子进程退出会产生僵尸问题;需要主动忽略信号来解决
            }

            close(serviceSock);
        }
    }
  • telnet简单测试服务器
    在这里插入图片描述

6. 服务器运行(多线程版)

class ThreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

在这里插入图片描述

  • 多线程代码如上所示
  • 在多线程这里不用进程关闭特定的文件描述符;因为Linux下的同一个进程中线程共用一张页表

3-3 客户端代码

  • TCP和UDP服务器代码都不需要显示绑定bind
  • tcp需要链接服务器( connect())
 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);1)sockfd:标识一个套接字。
(2)serv_addr:套接字s想要连接的主机地址和端口号。
(3)addrlen:name缓冲区的长度。
(4)如果链接或者绑定成功则返回 0,失败返回-1,错误原因存于errno 中。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
1)sockfd是socket()的返回值,文件描述符
(2)buf是接受数据的缓存区的指针
(3)len是发送数据的长度
(4)flags标志位,默认为0。
(5)返回值:成功则返回接收到的字符数,失败返回-1.
  • (长链接)代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"

using std::cout;
using std::endl;
using std::string;

static const size_t SIZE = 1024;
static void Usage(string proc)
{
    std::cout << "\nUsage: " << proc << " clientIP port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]); // 使用手册
        exit(1);
    }

    string ipClient = argv[1];
    uint16_t portClient = atoi(argv[2]);
    //  创建字节套接字
    int sockClient = socket(AF_INET, SOCK_STREAM, 0);
    if (sockClient < 0) //  创建失败
    {
        std::cerr << "client: 创建套接字失败 " << endl;
        exit(2);
    }
    //  不需要显示bind
    //  但需要建立链接
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(portClient);
    server.sin_addr.s_addr = inet_addr(ipClient.c_str());
    if (connect(sockClient, (struct sockaddr *)&server, sizeof server) < 0)
    {
        std::cerr << "client: 建立链接失败 " << endl;
        exit(3);
    }
    //  建立链接成功
    string line;
    while (true)
    {
        //  通信
        cout << "请输入# ";
        std::getline(std::cin, line);
        if (line == "quit")
            break;
        send(sockClient, line.c_str(), line.size(), 0); //  write(向sockClient中写数据);  发送数据

        char buffer[SIZE];
        ssize_t s = recv(sockClient, buffer, sizeof(buffer) - 1, 0); //  read;   接受数据
        if (s > 0)
        {
            buffer[s] = 0;
            cout << "server 回显# " << buffer << endl;
        }
        else if (s == 0) // 对写端关闭连接
        {
            cout << "shutdown, me too!" << endl;
            break;
        }
        else
        {
            std::cerr << "recv socket error" << endl;
            break;
        }
    }
    return 0;
}
  • 短连接代码块:
#include <iostream>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <cassert>
#include "Log.hpp"

using std::cout;
using std::endl;
using std::string;

static const size_t SIZE = 1024;
static void Usage(string proc)
{
    std::cout << "\nUsage: " << proc << " clientIP port\n"
              << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]); // 使用手册
        exit(1);
    }

    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    bool alive = false;
    int sock = -1;
    string line;
    while (true) 
    {
        if (!alive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if (sock < 0)
            {
                std::cerr << "socket error" << std::endl;
                exit(2);
            }
            // client 要不要bind呢?不需要显示的bind,但是一定是需要port
            // 需要让os自动进行port选择
            // 连接别人的能力!
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(serverport);
            server.sin_addr.s_addr = inet_addr(serverip.c_str());

            if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
            {
                std::cerr << "client: 创建套接字失败 " << endl;
                exit(2);
            }
            cout << "链接成功" << endl;
            alive = true;
        }
        //  链接上服务器
        cout << "请输入# ";
        std::getline(std::cin, line);
        if (line == "quit")
            break;

        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if (s > 0)
        {
            char buffer[1024];
            ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
            buffer[s] = 0;
            cout << "server 回显# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            cout << "shutdown, me too!" << endl;
            alive = false;
            close(sock);
        }
        else
        {
            std::cerr << "recv socket error" << endl;
            alive = false;
            close(sock);
        }
    }

    return 0;
}

长链接和短链接在线程池里结果更明显

3-4 结果展示

  • 多进程版服务器群聊功能
    在这里插入图片描述
  • 多线程版服务器群聊功能
    在这里插入图片描述

3-5 总代码链接

链接:
https://gitee.com/ding-xushengyun/linux__cpp/tree/master/tcp


http://www.niftyadmin.cn/n/77544.html

相关文章

dbForge Source Control for SQL Server 2.5.X Crack

SQL Server功能概述 的 dbForge 源代码管理 dbForge Source Control for SQL Server 是一个可视化的 SSMS 插件&#xff0c;具有简单易用的界面&#xff0c;可帮助您轻松跟踪 SQL Server 数据库对象中的更改内容、更改时间和原因。该工具使您能够将数据库连接到多个版本控制系统…

[LeetCode]1237. 找出给定方程的正整数解

题目链接&#xff1a;https://leetcode.cn/problems/find-positive-integer-solution-for-a-given-equation/description/ 题目描述&#xff1a; 样例1&#xff1a; 输入&#xff1a;function_id 1, z 5 输出&#xff1a;[[1,4],[2,3],[3,2],[4,1]] 解释&#xff1a;functi…

[架构之路-109]-《软考-系统架构设计师》-软件架构设计-2-软件架构概述:架构风格

引言建筑风格指建筑设计中在内容和外貌方面所反映的特征&#xff0c;主要在于建筑的平面布局、形态构成、艺术处理和手法运用等方面所显示的独创和完美的意境。建筑风格因受时代的政治、社会、经济、建筑材料和建筑技术等的制约以及建筑设计思想、观点和艺术素养等的影响而有所…

强化学习基础概念

强化学习入门 入门学习第一周&#xff1a;基础概念 经验回放&#xff1a; 将sss,agent当前步的action环与境的交互rrr以及下一步的状态st1s_{t1}st1​组成的四元组[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wxhVd0dn-1676710992983)(null)] 组…

斐波那契数列(递归+迭代)

目录什么是斐波那契数列递归写法使用递归写法的缺点迭代写法(效率高)什么是斐波那契数列 斐波那契数列&#xff08;Fibonacci sequence&#xff09;&#xff0c;又称黄金分割数列&#xff0c;因数学家莱昂纳多斐波那契&#xff08;Leonardo Fibonacci&#xff09;以兔子繁殖为例…

Python基础2

1. python函数定义 函数定义语法&#xff1a; def 函数名&#xff08;传入参数&#xff09;&#xff1a; 函数体 return 返回值 —————————————— 参数如果不需要&#xff0c;可以省略返回值如果不需要&#xff0c;可以省略函数必须先定义在使用 注意&#xff…

保姆级教会你SQL窗口函数

SQL窗口函数SQL中有一个很重要的函数被我们经常用到查询语句中&#xff0c;这个函数就是窗口函数&#xff0c;今天我们就来看看什么是窗口函数以及它的使用方法吧&#xff01;认识窗口函数窗口函数是一种特殊类型的SQL函数&#xff0c;它可以在指定的窗口范围内计算结果。窗口函…

Leetcode.1237 找出给定方程的正整数解

题目链接 Leetcode.1237 找出给定方程的正整数解 Rating : 1405 题目描述 给你一个函数 f(x, y)和一个目标结果 z&#xff0c;函数公式未知&#xff0c;请你计算方程 f(x,y) z所有可能的正整数 数对 x和 y。满足条件的结果数对可以按任意顺序返回。 尽管函数的具体式子未知&…