网络编程套接字(上篇)UDP实现简易多人聊天室

news2024/9/23 21:28:11

目录

背景知识

主机间通信本质

socket

端口号特点:

为什么不用进程pid?

网络字节序 

socket编程接口API

 sockaddr结构

​编辑 简单UDP网络程序

了解UDP协议

简易多人聊天室实现

服务端代码:

客户端代码:


背景知识

主机间通信本质

各自主机上的进程之间相互交互数据

IP地址完成主机与主机之间的通信

主机上各自的通信进程分别是发送数据和接收数据的一方

socket

IP地址:标识主机唯一性(4字节32位)

端口号port:标识了主机上的进程唯一性(2字节16位)

那么 IP地址 + 端口号 就能够标识网络上的某一台主机的某一个进程,将IP地址+端口号称为socket对,之间用冒号分隔,如 源IP:源端口号  目的IP:目的端口号。

端口号特点:

端口号 (port) 是传输层协议的内容 .
端口号是一个2字节16位的整数;
端口号用来标识一个进程

一个进程可以绑定多个端口号

一个端口号只能被一个进程占用

OS内部用哈希表存储端口号,通过哈希表映射,使用端口号可以快速找到进程

为什么不用进程pid?

避免进程管理和网络通信的强耦合 ,同时端口号标识的进程是要进行网络通信的网络进程,没有端口号则说明是本地进程,不进行网络通信,就好比身份证号与学号,独立分配,便于管理。

网络字节序 

内存中的多字节数据相对于内存地址有大端和小端之分 , 磁盘文件中的多字节数据相对于文件中的 偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。

在网络中发送主机将发送缓冲区的数据按内存从低到高地址顺序发出,接收主机保存数据在接收缓冲区从低到高。

TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。

所有主机都会按照这个TCP/IP规定的网络字节序来发送/接收数据,如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库中的四个函数做网络字节序和主机字节序的转换,当主机是小端字节序,函数内部才会将参数做转换,否则原封不动返回。

h表示host,n表示network,l表示32位长整数,s表示16位短整数。

socket编程接口API

TCP或UDP,客户端/服务器

创建socket文件描述符:

int socket(int domain, int type, int protocol);

TCP或UDP,服务器

绑定端口号:

int bind(int socket, const struct sockaddr *address,
 socklen_t address_len);

TCP,服务器

监听socket:

int listen(int socket, int backlog);

接收请求:

int accept(int socket, struct sockaddr* address,
 socklen_t* address_len);

TCP,客户端

建立连接:

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

 sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6(网络通信)以及UNIX Domain Socket(域间通信)。在网络中有描述网络地址的结构体sockaddr。

struct sockaddr_in结构体是描述网络通信

sockaddr_un结构体是描述域间通信

两个类型都可以用struct sockaddr 类型表示,在使用接口时进行强制类型转换就行了,因为他们前面16位字段表示地址类型,底层根据16位地址类型进行类型转换就可以适用不同类型的地址了。

在内核代码中可以看到sockaddr_in描述网络通信地址的结构体中有字段表示IP地址和端口号port:

 简单UDP网络程序

了解UDP协议

UDP协议是传输层协议

特点:

无连接:不用提前建立连接,类似邮箱,谁都能往邮箱发消息

不可靠传输:与TCP相比没有超时重传等机制保证丢包重传,数据传输是不可靠的

面向数据报:意味着应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,比如用UDP传输100个字节的数据如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom,每次接收10个字节

支持全双工:读写使用同一个套接字,UDP调用sendto会将数据直接交给内核,内核直接将数据交给网络层,UDP有接收缓冲区,但是不能保证发送报文的顺序和接收的一致,缓冲区满了之后再到达的报文会被丢弃。

简易多人聊天室实现

服务端代码:

1.创建套接字int socket(int domain,int type,int protocol):

参数说明:

domain:域,本地(AF_UNIX)或网络(AF_INET)

type:报文类型,流式(SOCK_STREAM),用户数据报(SOCK_DGRAM)

protocol:协议类型,在网络应用中填充0

返回值:返回一个文件描述符

2.绑定网络信息,先填充协议家族,指明ip和port

先填充基本信息到sockaddr_in网络地址结构体

2.1填充协议家族,域:.sin_family

2.2填充端口号:.sin_port,需用htons转化

2.3填充IP地址sin_addr.s_addr

使用inet_addr函数指定填充确定的IP,内部自动调用hton

IP地址填充INADDR_ANY表示绑定服务器上的所有IP,因为云服务器禁止绑定确定的IP,为了安全性云服务器上的IP是模拟出来的。

IP互相转化函数

inet_ntoa:将四字节ip转化为点分十进制字符串

返回的地址存储在静态存储区,下一次调用的时候会覆盖上一次的结果,这个函数可能是非线程安全函数,取决不同平台的实现

 inet_addr:将点分十进制字符串转化成四字节IP,内部自动hton,这个函数未必是线程安全函数

其他IP地址转化函数也类似:

在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题:

2.4bind网络信息

参数说明:
sockfd:绑定的文件描述符

addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。

addrlen:传入的addr结构体的长度。

3.收发消息

往套接字发送消息:

使用sendto函数往套接字发送消息:

表示往sockfd套接字发送buf中的len长度的内容,flag填0表示像write一样阻塞读取,后两个参数是输入型参数,表示要发送的对端主机信息(IP,端口号)。

 从套接字收消息:

这个函数与上面类似,将从sockfd套接字中读取len长度字节数据到buf中,后两个参数主要是做输出型参数,可以从中提取消息来源的主机信息(IP,端口号)。

注意:recvfrom和sendto是专门用于udp收发用户数据报的

实现多人聊天室的功能:让服务器作为数据的中间收发者,客户端发送数据给服务端,服务端再将数据广播给所有(除了发送方)客户端,为了维护多个客户端信息,可以在服务端收取数据的时候提取发送方的主机信息,将其存储在unordered_map中方便广播。

实现日志功能:引入日志,使用可变参数函数实现日志函数,方便格式化输出,顺便包含各式头文件方便服务器代码和客户端代码引用。

在log.hpp中:

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
#include <string>
#include <unordered_map>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

using namespace std;
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    char *name = getenv("USER");
    char logInfo[1024];
    va_list ap;
    va_start(ap, format);                            // 第一个知道类型的参数format
    vsnprintf(logInfo, sizeof(logInfo), format, ap); // 以format格式写入logInfo,自动传递参数
    va_end(ap);                                      // ap=NULL
    FILE *out = level == FATAL ? stderr : stdout;
    fprintf(out, "%s | %u | %s | %s\n",
            log_level[level],
            (unsigned int)time(nullptr),
            name == nullptr ? "unknown" : name,
            logInfo);
}

服务器代码实现:

#include "log.hpp"

class udpServer
{
private:
    uint16_t port_;
    string ip_;
    int sockfd_;
    unordered_map<string, struct sockaddr_in> users_; // ip:port peer
public:
    udpServer(int port, string ip = "")
        : port_((uint16_t)port), ip_(ip), sockfd_(-1)
    {
    }
    ~udpServer() {}

    void init()
    {
        // 1.创建socket,打开文件
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            logMessage(FATAL, "socket : %s:%d", strerror(errno), sockfd_);
            exit(1);
        }
        // 2.绑定网络信息,指明ip和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        // 2.1填充协议家族,域
        local.sin_family = AF_INET;
        // 2.2填充服务器对应的端口号
        local.sin_port = htons(port_);
        // 2.3填充IP地址     //将点分十进制转化成四字节ip
        local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());

        // 2.4绑定网络信息
        if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) == -1)
        {
            logMessage(FATAL, "bind : %s:%d", strerror(errno), sockfd_);
        }
        cout << "create server success" << endl;
    }

    void start()
    {
        char inbuffer[1024];
        char outbuffer[1024];
        while (1)
        {
            // 往peer写入客户端信息
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1,
                                 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                inbuffer[s] = '\0';
            }
            else
            {
                logMessage(WARINING, "recvfrom : %s:%d", strerror(errno), sockfd_);
                continue;
            }
            // 取出客户端的ip和port
            string peerIp = inet_ntoa(peer.sin_addr);
            uint32_t peerPort = ntohs(peer.sin_port);
            logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);
            broadcastMessage(peerIp, peerPort, peer, inbuffer); // 如果是新用户 就先添加用户
        }
    }

    void broadcastMessage(string peerIp, uint32_t peerPort, struct sockaddr_in peer, char *send)
    {
        string socket = peerIp + ":";
        socket += to_string(peerPort);
        auto it = users_.find(peerIp);
        if (it == users_.end())
            users_.insert({socket, peer});

        string message="FROM";
        message += "[";
        message += peerIp;
        message += ":";
        message += to_string(peerPort);
        message += "]";
        message += " echo# ";
        message += send;
        for (auto &user : users_)
        {
            if (user.first != socket)
            {
                sendto(sockfd_, message.c_str(), message.size(),
                       0, (struct sockaddr *)&user.second, sizeof(user.second));
            }
        }
    }
};

int main(int argc, char *argv[])
{
    using namespace std;
    if (argc != 2 && argc != 3)
    {
        cout << "Usage:\n\t " << argv[0] << " port [ip]" << endl;
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    string ip;
    if (argc == 3)
        ip = argv[2];
    udpServer svr(port, ip);
    svr.init();
    svr.start();
    return 0;
}

客户端代码:

客户端工作较为简单,直接创建套接字,然后就可以往服务器收发消息了,

客户端不需要bind,指的是不需要用户自己bind端口信息,os会自动bind,而且客户端不能绑定指定端口,因为端口可能被别的客户端使用,导致客户端无法启动,而服务端提供的服务需要被所有人知道,所以不能随便改变端口号,需要显式bind指定端口。

当客户端首次调用sendto函数时,函数内部会自动绑定(bind)

为了避免多个客户端收发的线程一样导致输入输出的卡顿,创建一个新线程用于收取服务器发来的消息,原来的主线程用于发消息给服务器。

#include "log.hpp"

void *recvAndPrint(void *args)
{
    int sockfd = *(int *)args;
    char buffer[1024];
    struct sockaddr_in temp;
    socklen_t len = sizeof(temp);
    while (1)
    {
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1,
                             0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << buffer<< endl;
        }
    }
    return nullptr;
}
struct sockaddr_in server;

int main()
{
    string ip = "127.0.0.1";//表示本地环回,给本主机发消息
    uint16_t port = 8080;
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

    bzero(&server, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = inet_addr(ip.c_str());

    // 接收服务器信息的线程
    pthread_t t;
    pthread_create(&t, nullptr, recvAndPrint, (void *)&sockfd);

    // 往服务器发送信息
    string buffer;
    while (1)
    {
        cout << "Please Enter# ";
        std::getline(std::cin, buffer);
        sendto(sockfd, buffer.c_str(), buffer.size(),//首次调用时自动bind
               0, (struct sockaddr *)&server, sizeof(server));
    }
    close(sockfd);
    return 0;
}

效果演示(3人通信):因为客户端发消息才会bind,所以后发消息的客户端看不到最先发消息的客户端的前面的消息。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/351183.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL第六章第四次作业

学生表&#xff1a;Student (Sno, Sname, Ssex , Sage, Sdept) 学号&#xff0c;姓名&#xff0c;性别&#xff0c;年龄&#xff0c;所在系 Sno为主键 课程表&#xff1a;Course (Cno, Cname,) 课程号&#xff0c;课程名 Cno为主键 学生选课表&#xff1a;SC (Sno, Cno, Score)…

java切换版本等注意事项

启动java工具&#xff0c;命令行为&#xff1a;java -jar xxx.jar 注意burpsuite&#xff0c;behinder需要jdk&#xff0c;如果是这个例子&#xff0c;jdk-11.0.12&#xff0c;shiro_attack_2.2需要jdk版本jdk1.8.0_291。 jdk版本切换方式&#xff1a; 编辑&#xff5e;目录…

教育舆情监测关键词有哪些,TOOM教育舆情监测系统流程?

教育舆情监测是指对教育领域的舆情进行收集、分析和处理的过程。舆情是指公众在各种渠道上对教育政策、教育机构、教育事件等方面的言论、态度和情绪。通过对教育舆情的监测和分析&#xff0c;可以了解公众对教育行业的看法和反应&#xff0c;提高对教育行业的管控能力&#xf…

MFC入门

1.什么是MFC?全称是Microsoft Foundation Class Library&#xff0c;我们称微软基础类库。它封装了windows应用程序的各种API以及相关机制的C类库MFC是一个大的类库MFC是一个应用程序框架MFC类库常用的头文件afx.h-----将各种MFC头文件包含在内afxwin.h-------包含了各种MFC窗…

AcWing语法基础课笔记 第六章 C++中的函数

第六章 C中的函数 函数让代码变得更加简洁。 ——闫学灿 目录 1.函数基础 1.1编写函数 1.2调用函数 1.3形参和实参 1.4函数的形参列表 1.5函数返回类型 1.6局部变量、全局变量与静态变量 2.参数传递 传值参数​编辑 2.2传引用参数 2.3数组形参…

从事1年软件测试,只会功能测试,想进一步学习,有没有好的建议呢?

作为一个在软件测试领域奋斗10年的老人&#xff0c;我前8年先后在不同的互联网公司担任高级软件测试工程师&#xff0c;测试主管等职&#xff0c;所以这么多年下来&#xff0c;也算是身经百战&#xff0c;阅人无数了。 根据粉丝的提问&#xff0c;得到你当前的状态是1&#xf…

中国天气——低纬度和高纬度环流复习笔记

低纬度和高纬度环流 低纬度大气运动基本特征 中低纬度大气运动的差别 低纬度的地转参数f很小&#xff0c;天气尺度系统具有非地转特性&#xff0c;但吃到以外的行星尺度还是有准地转近似特性因此中纬度有效的气压场和温度场分析系统运动和变化的方法在低纬度不再适用&#x…

虚拟现实三维数字沙盘电子沙盘可视化交互开发教程第5课

虚拟现实三维数字沙盘电子沙盘可视化交互开发教程第5课 设置system.ini 如下内容 Server122.112.229.220 userGisTest Passwordchinamtouch.com 该数据库中只提供 成都市火车南站附近的数据请注意&#xff0c;104.0648,30.61658 在鼠标指定的位置增加自己的UI对象&#xff1…

实现8086虚拟机(一)——基本框架

文章目录基本框架几点说明&#xff1a;在 实现8086汇编编译器&#xff08;四&#xff09;——生成可执行程序 一文中&#xff0c;我已经实现了一个编译器&#xff0c;可以将汇编语言汇编成二进制程序。这几篇文章来讲述如何实现虚拟机&#xff0c;也就是执行这个程序的“机器”…

LabVIEW错误-2147220623:最大内存块属性不存在

LabVIEW错误-2147220623&#xff1a;最大内存块属性不存在在使用NI Linux实时操作系统目标中&#xff0c;使用系统属性节点和分布式系统管理器&#xff08;DSM&#xff09;&#xff0c;但遇到一些问题&#xff1a;它未正确报告系统上的可用物理内存量。在NI Linux实时系统上出现…

深入浅出带你学习无列名注入

前言 大家对于SQL注入一定不陌生&#xff0c;我们常用的SQL注入方法是通过information_schema这个默认数据库来实现&#xff0c;可是你有没有想过&#xff0c;如果过滤了该数据库那么我们如何进行SQL语句的查询呢&#xff0c;本文就带给大家如何通过不使用information_schema来…

MyBatis详解2——增删改查操作

一、SpringBoot单元测试 1.1什么是单元测试 单元测试是指对软件中的最小测试单元进行检查和验证的过程。 执行单元测试就是为了证明某段代码的执行结果是否符合我们的预期。如果测试通过则是符合预期&#xff0c;否则测试失败。 1.2单元测试的好处 1.单元测试不用启动Tomca…

全球十大资质正规外汇期货平台排行榜(最新版汇总)

外汇期货简称为FxFut&#xff0c;是“Forex Futures”的缩写&#xff0c;是在集中形式的期货交易所内&#xff0c;交易双方通过公开叫价&#xff0c;以某种非本国货币买进或卖出另一种非本国货币&#xff0c;并签订一个在未来的某一日期根据协议价格交割标准数量外汇的合约。 …

Pycharm开发工具的安装和基础使用

数据来源 01 Python开发环境 Pycharm集成开发工具(DE)&#xff0c;是当下全球Pthn开发者&#xff0c;使用最频繁的工具软件。 绝大多数的 Python程序&#xff0c;都是在 Pycharm工具内完成的开发。 Pycharm工具下载 首先&#xff0c;我们先下载并安装它&#xff1a;打开网站…

罗列几个提升WPF应用程序冷启动性能的方法!(Part 2)

在上文中&#xff08;点击这里回顾>>&#xff09;&#xff0c;我们主要介绍了针对三个技术的WPF应用程序性能提升&#xff0c;本文将着重介绍针对DevExpress WPF界面控件研发的应用程序如何提升性能&#xff01;有用控件推荐~DevExpress WPF拥有120个控件和库&#xff0c…

PostgreSQL的学习心得和知识总结(一百二十三)|深入理解PostgreSQL数据库开源扩展pg_dirtyread的使用场景和实现原理

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

大彩 串口屏

资料下载 视频 屏幕程序创建 创建 主界面设置 实现按钮和文本的添加&#xff0c;实现画面的切换 下面注释4有点问题&#xff0c;切换画面还是会下传指令集&#xff0c;只是无法在软件中进行指令集的设置了 按钮界面 首先第一步同上添加背景图片&#xff0c;然后添加…

性能VS功能,同为测试又有哪些不一样?

我们在求职的时候&#xff0c;发现有的是招聘的功能测试&#xff0c;有的招聘的是性能测试&#xff0c;那么功能测试和性能测试的区别是什么呢&#xff1f; 侧重点不同 功能测试的侧重点是功能是否满足客户需求。 比如说我们拿到一个节假日搞活动的需求&#xff0c;这个需求…

【订阅】订阅MySql集简云连接器同步报销审批数据至MySql数据库

方案场景 企业在实现数字化转型的道路上&#xff0c;因企业多个系统孤立数据割断&#xff0c;数据互通成为企业率先解决的最大问题&#xff0c;依靠钉钉OA审批&#xff0c;企业通过审批后手动录入到企业的自建系统&#xff0c;然后再同步到MySQL数据库&#xff0c;这种方式不仅…

WPF MVVM系统入门-下

WPF MVVM系统入门-下 CommandManager 接上文WPF MVVM系统入门-上&#xff0c;我们想把Command放在ViewModel中&#xff0c;而不是Model中&#xff0c;可以将CommandBase类改为 public class CommandBase : ICommand {public event EventHandler? CanExecuteChanged{add { C…