socket实现简单的网络聊天服务器和客户端(UDP)

在日常应用中有很多关于socket网络通信的例子,例如局域网内打游戏,使用浏览器看视频,用QQ软件聊天等。可以说socket是底层抽象给应用层所使用的一套接口。网络通信的传输方式有两种,一种是基于TCP(数据可靠传输),另一种是基于UDP(数据不可靠,一般用于实时视频传输)。

        1.基于TCP的网络编程


         由于基于TCP的套接字是面向连接的,因此又称为基于流(Stream)的套接字。TCP是Transmission Control Protocol(传输控制协议)的简写,译为“对数据传输过程的控制”。那么,在网络交互过程中,服务器端和客户端要始终保持连接,不能断开。TCP协议会重发一切出错数据,保证数据的完整性和顺序性。缺点就是消耗资源比较大。

        服务器编程步骤:

        (1)创建socket描述符socket()。

        (2)准备通信地址 struct sockaddr_in。

        (3)绑定bind()。

        (4)监听listen()。

        (5)等待客户端的连接accept()。

        (6)read/write。

        (7)关闭socket。

        客户端编程步骤:

        (1) 创建socket描述符socket()。

        (2)准备通信地址struct sockaddr_in。

        (3)连接到服务器connect()。

        (4)read/write。

        (5)关闭socket。 

        2.相关API讲解


        1.inet_aton函数

        函数原型:inet_aton(Const char *cp,struct in_addr *inp);

        参数cp:  字符串类型的IP地址。

        inp:  struct in_addr*类型的数据。

        函数作用:将字符串类型的cp转换成struct in_addr*类型的数据并赋值给inp变量。

        返回值:成功则返回非0值,失败则返回0。

        2.listen函数

        函数原型:int listen(int sockfd, int backlog);

        参数说明:

        sockfd: socket描述符。

        backlog:  未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。在进程正在处理一个连接请求时,可能还存在其他连接请求。因为TCP连接是一个过程,所以可能存在一种半连接状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果出现这种情况,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。若服务器端的未决连接数已达此限值,此时,如果有客户端使用函数connect()连接服务器,那么connect()函数就会返回-1,errno的值为ECONNREFUSED。

        返回值:成功则返回0,失败则返回-1

        3.accept函数

        函数原型:int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

        从sockfd所标识的未决连接请求队列中,提取一个连接请求,同时创建一个新的套接字用于在该连接中通信,返回值为该套接字的描述符。通常情况下如果连接请求队列中没有请求,accept会阻塞等待。

        参数说明:

        sockfd:  套接字描述符。

        addr和addrlen:用于输出连接请求发起者的地址信息,注意这两个参数一定不能为空。

        返回值:成功则返回通信套接字描述符,失败则返回-1。

        4.recv函数

        函数原型:ssize_t recv(int sockfd,void *buf,size_t len, int flags);

        参数说明:

        sockfd: 套接字描述符

        buf, len:接收len个字节到buf所指向的缓冲区中。

        flags:  通常情况下设置为0,表示没有数据读取时,客户端进程处于阻塞等待状态。

        返回值:成功则返回实际接收到的字节数,失败则返回-1。

        5.send函数

        函数原型:size_t send(int sockfd,const void *buf,size_t len, int flags);

        参数说明:

        sockfd: 套接字描述符。

        buf,  len:  发送len个字节到buf所指向的缓冲区中。

        flags:  通常情况设置为0,表示没有数据需要发送时,客户端进程处于阻塞等待状态。

        返回值:成功则返回实际发送的字节数,失败则返回-1。

        6.recvfrom函数

        函数原型:ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen)

        参数说明:
        sockfd:  套接字描述符

        buf:  接收数据缓冲区

        len:  期望接收数据长度。

        flags:  默认取0。

        src_addr:  获取客户端IP。

        addrlen:  前一个参数对应结构体的大小,切记该值不取0。

        返回值:成功则返回实际发送的字节数,失败则返回-1。

        7.sendto函数

        函数原型:ssize_t sendto(int sockfd,void *buf,size_t len,int flags,struct sockaddr *dest_addr, socklen_addrlen);

        参数说明:
        sockfd:  套接字描述符。

        buf:  要发送数据的缓冲区。

        len:  期望发送的字节数。

        flags:  默认取0。

        dest_addr:  目标主机IP。

        addrlen:  前一个参数对应结构体的大小,切记该值不取0。

        返回值: 成功则返回实际发送的字节数,失败则返回-1。

        前面已经介绍过字节序的概念,其中网络字节序采用的是大端模式,而目前的计算机8086平台采用的是小端模式,所以进行网络通信时,还需要一些大小端模式的转换函数,见下表。

如果不用多线程的话,在没法消息的情况下,收消息就会被阻塞,所以必须用多线程实现

MakeFile:

.PHONY:all
all:udp_client udp_server

udp_client:udp_client.cc
	g++ -o $@ $^ -std=c++11 -lpthread
udp_server:udp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udp_client udp_server

写好makefile后直接在linux下make就可以完成编译

log.hpp:

这是一个日志文件,用于记录程序的执行情况

#pragma once

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

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

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

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    // va_list ap;
    // va_start(ap, format);
    // while()
    // int x = va_arg(ap, int);
    // va_end(ap); //ap=nullptr
    char stdBuffer[1024]; //标准部分
    time_t timestamp = time(nullptr);
    // struct tm *localtime = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);

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

    // FILE *fp = fopen(LOGFILE, "a");
    printf("%s%s\n", stdBuffer, logBuffer);
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

服务器端实现

udp_Server.hpp

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1)
    {
    }
    bool initServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        //  1. 创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET->FP_INET
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2. bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址
        // 每一个区域取值范围是[0-255]: 1字节 -> 4个区域
        // 理论上,表示一个IP地址,其实4字节就够了!!
        // 点分十进制字符串风格的IP地址 <-> 4字节
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!
        local.sin_port = htons(_port);
        // 1. 同上,先要将点分十进制字符串风格的IP地址 ->  4字节
        // 2.  4字节主机序列 -> 网络序列
        // 有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据
        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", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        // done
        return true;
    }
    void Start()
    {
        // 作为一款网络服务器,永远不退出的!
        // 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        // echo server: client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        for (;;)
        {
            // 注意:
            //  peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            // 输入: peer 缓冲区大小
            // 输出: 实际读到的peer
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;
            // start. 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 我们目前数据当做字符串
                // 1. 输出发送的数据信息
                // 2. 是谁??
                //  你发过来的字符串是指令 ls -a -l, rm -rm ~
                // if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
                // {
                //     std::string err_message = "坏人.... ";
                //     std::cout << err_message << buffer << std::endl;
                //     sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                //     continue;
                // }
                // FILE *fp = popen(buffer, "r");
                // if (nullptr == fp)
                // {
                //     logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                //     continue;
                // }
                // while (fgets(result, sizeof(result), fp) != nullptr)
                // {
                //     cmd_echo += result;
                // }
                // fclose(fp);
                uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来的!
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
                logMessage(NORMAL, "key: %s", key);
                // std::string key_string = key;
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // exists
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
            // 分析和处理数据,TODO
            // end. 写回数据
            // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
            // sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);
            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _users;
    std::queue<std::string> messageQueue;
};

#endif

udp_Server.cc

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

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

// ./udp_server ip port //云服务器的问题 bug??
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(1);
    }
    // std::string ip = argv[1];
    uint16_t port = atoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->initServer();
    svr->Start();
    return 0;
}

客户端实现

udp_client.hpp

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

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

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") : _port(port), _ip(ip), _sock(-1)
    {
    }
    bool initServer()
    {
        // 从这里开始,就是新的系统调用,来完成网络功能啦
        //  1. 创建套接字
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET->FP_INET
        if (_sock < 0)
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }
        // 2. bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址
        // 每一个区域取值范围是[0-255]: 1字节 -> 4个区域
        // 理论上,表示一个IP地址,其实4字节就够了!!
        // 点分十进制字符串风格的IP地址 <-> 4字节
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络!
        local.sin_port = htons(_port);
        // 1. 同上,先要将点分十进制字符串风格的IP地址 ->  4字节
        // 2.  4字节主机序列 -> 网络序列
        // 有一套接口,可以一次帮我们做完这两件事情, 让服务器在工作过程中,可以从任意IP中获取数据
        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", errno, strerror(errno));
            exit(2);
        }
        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        // done
        return true;
    }
    void Start()
    {
        // 作为一款网络服务器,永远不退出的!
        // 服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了!
        // echo server: client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        for (;;)
        {
            // 注意:
            //  peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            // 输入: peer 缓冲区大小
            // 输出: 实际读到的peer
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;
            // start. 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                buffer[s] = 0; // 我们目前数据当做字符串
                // 1. 输出发送的数据信息
                // 2. 是谁??
                //  你发过来的字符串是指令 ls -a -l, rm -rm ~
                // if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
                // {
                //     std::string err_message = "坏人.... ";
                //     std::cout << err_message << buffer << std::endl;
                //     sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                //     continue;
                // }
                // FILE *fp = popen(buffer, "r");
                // if (nullptr == fp)
                // {
                //     logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                //     continue;
                // }
                // while (fgets(result, sizeof(result), fp) != nullptr)
                // {
                //     cmd_echo += result;
                // }
                // fclose(fp);
                uint16_t cli_port = ntohs(peer.sin_port);      // 从网络中来的!
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
                logMessage(NORMAL, "key: %s", key);
                // std::string key_string = key;
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    // exists
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }
            }
            // 分析和处理数据,TODO
            // end. 写回数据
            // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);
            // sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr *)&peer, len);
            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));
            }
        }
    }
    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

private:
    // 一个服务器,一般必须需要ip地址和port(16位的整数)
    uint16_t _port;
    std::string _ip;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _users;
    std::queue<std::string> messageQueue;
};

#endif

udp_client_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>
#include <memory>
#include "thread.hpp"

// 发现:无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰

uint16_t serverport = 0;
std::string serverip;

static void usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
              << std::endl;
}

static void *udpSend(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    std::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 << "请输入你的信息# "; //标准错误 2打印
        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 *udpRecv(void *args)
{
    int sock = *(int *)((ThreadData *)args)->args_;
    std::string name = ((ThreadData *)args)->name_;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << 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 << "socket error" << std::endl;
        exit(2);
    }
    serverport = atoi(argv[2]);
    serverip = argv[1];
    // client要不要bind??要,但是一般client不会显示的bind,程序员不会自己bind
    // client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢??
    // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)

    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));
    // sender->name();
    sender->start();
    recver->start();

    sender->join();
    recver->join();

    close(sock);

    // std::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[1024];
    // 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);
    //     ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
    //     if(s > 0)
    //     {
    //         buffer[s] = 0;
    //         std::cout << "server echo# " << buffer << std::endl;
    //     }
    // }
    return 0;
}

至此就完成了一个简单的基于UDP的网络群聊功能

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值