网络编程套接字——实现简单的UDP网络程序

目录

1、预备知识

1.1、认识端口号

1.2、端口号 vs 进程pid

1.3、认识TCP协议

1.4、认识UDP协议

1.5、网络字节序

2、socket编程接口

2.1、socket常见API

2.2、sockaddr结构

3、实现一个简易的UDP服务器和客户端通信

log.hpp

UdpServer.hpp

UdpClient.cc

Main.cc

makefile


1、预备知识

1.1、认识端口号

首先我们先来解决一个问题,在进行网络通信的时候,是不是我们的两台机器在进行通信呢?

答案肯定是不是的,在网络协议中的下三层,主要解决的是,数据安全可靠的运送到远端机器,但是使我们的用户使用应用层软件,完成数据发送和接收的,软件对应的自然就是进程,所以,我们日常网络通信的本质:就是进程间通信!

那么问题又来了,我们两台主机在进行通信时,传输层的上层应用层不止有一个应用软件,该如何区分呢?用的就是端口号。

在公网上,IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程,所以:IP:Port = 标识全网唯一的一个进程。

那么我们来总结一下端口号的特性;

· 端口号是一个2字节16位的整数

· 端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪个进程处理

· IP地址+端口号能够标识网络上的某一台主机的某一个进程

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

1.2、端口号 vs 进程pid

pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?

这个问题也很简单,因为网络是晚于操作系统的,而操作系统的每个进程都需要有pid,但是却不失所有的进程都要网络通信,另外,也是追求软件工程低耦合高内聚思想,创建一个端口号,实现了系统和网络功能的解耦。

再做一点小小的扩充,我们的客户端是如何知道服务器的端口号是多少的?

答:每一个服务的端口号都必须是众所周知,精心设计,被客户端知晓的。

那么这该如何实现呢?

如图所示,其实在传输层,有一张哈希表存储端口号,而进程会绑定哈希表中的端口号,这样就实现了端口号的要求,但是这里还有一些细节:

一个进程可以绑定多个端口号,但是一个端口号不可以被多个进程绑定,如果被多个进程绑定就会让端口号无法决定该将信息发送给哪个进程!

1.3、认识TCP协议

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

· 传输层协议

· 有连接

· 可靠传输

· 面向字节流

1.4、认识UDP协议

此处也是对UDP(User Datagram Protocol 用户数据协议)有一个直观的认识,细节后面讨论。

· 传输层协议

· 无连接

· 不可靠传输

· 面向数据报

1.5、网络字节序

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

· 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
· 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
· 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
· TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
· 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
· 如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

#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);

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2、socket编程接口

2.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);

2.2、sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而,各种网络协议的地址格式并不相同。

· 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结构体指针做为参数;


3、实现一个简易的UDP服务器和客户端通信

log.hpp

#pragma once#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>#define SIZE 1024#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4#define Screen 1
#define Onefile 2
#define Classfile 3#define LogFile "log.txt"class Log
{
public:Log(){printMethod = Screen;path = "./log/";}void Enable(int method){printMethod = method;}std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}// void logmessage(int level, const char *format, ...)// {//     time_t t = time(nullptr);//     struct tm *ctime = localtime(&t);//     char leftbuffer[SIZE];//     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),//              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,//              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);//     // va_list s;//     // va_start(s, format);//     char rightbuffer[SIZE];//     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);//     // va_end(s);//     // 格式:默认部分+自定义部分//     char logtxt[SIZE * 2];//     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);//     // printf("%s", logtxt); // 暂时打印//     printLog(level, logtxt);// }void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen:std::cout << logtxt << std::endl;break;case Onefile:printOneFile(LogFile, logtxt);break;case Classfile:printClassFile(level, logtxt);break;default:break;}}void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"if (fd < 0)return;write(fd, logtxt.c_str(), logtxt.size());close(fd);}void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);}~Log(){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// printf("%s", logtxt); // 暂时打印printLog(level, logtxt);}private:int printMethod;std::string path;
};// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }//     va_end(s); //s = NULL
//     return sum;
// }

UdpServer.hpp

#pragma once#include <iostream>
#include <string>
#include <string.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"typedef std::function<std::string(const std::string &, const std::string&, uint16_t)> func_t;Log log;enum{SOCKET_ERR=1,BIND_ERR=2
};uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;class UdpServer{
public:UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning_(false){}void Init(){// 1. 创建udp socket//  Udp的socket是全双工的,允许被同时读写的sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){log(Fatal, "socket creat error, sockfd: %d", sockfd_);exit(SOCKET_ERR);}log(Info, "socket create success, sockfd: %d", sockfd_);// 2. bind socketstruct 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);int n = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local));if(n < 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 CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport){auto iter = online_user_.find(clientip);if(iter == online_user_.end()){online_user_.insert({clientip, client});std::cout << "[" << clientip << ":" << clientport << "] add to online user. " << std::endl;}}void Broadcast(const std::string&info, const std::string clientip, uint16_t clientport){for(const auto &user : online_user_){std::string message = "[";message += clientip;message += ":";message += std::to_string(clientport);message += "]# ";message += info;socklen_t len = sizeof(user.second);sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);}}void Run() //对代码进行分层{isrunning_ = true;char inbuffer[size];while(isrunning_){struct sockaddr_in client;socklen_t len = sizeof(client);ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 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);CheckUser(client, clientip, clientport);std::string info = inbuffer;Broadcast(info, clientip, clientport);}}~UdpServer(){if(sockfd_ > 0) close(sockfd_);}private:int sockfd_;      // 网络文件描述符std::string ip_; // 任意地址bind 0uint16_t port_;   // 表明服务器进程的端口号bool isrunning_;std::unordered_map<std::string, struct sockaddr_in> online_user_;
};

UdpClient.cc

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"using namespace std;void Usage(std::string proc)
{std::cout << "\n\tUsage:" << proc << "serverip serverport\n" << std::endl;
}struct ThreadData
{struct sockaddr_in server;int sockfd;std::string serverip;
};void *recv_message(void *args)
{//OpenTerminal();ThreadData *td = static_cast<ThreadData *>(args);char buffer[1024];while(true){memset(buffer, 0, sizeof(nuffer));struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);if(s > 0){buffer[s] = 0;cerr << buffer << endl;}}
}void *send_message(void *args)
{ThreadData *td = static_cast<ThreadData *>(args);socklen_t len = sizeof(td->server);string message;std::string welcome = td->serverip;welcome += "coming...";sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);while(true){cout << "Please Enter@ ";getline(cin, message);// 1. 数据 2. 给谁发sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);}
}//多线程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);struct ThreadData td;bzero(&td.server, sizeof(td.server));td.server.sin_family = AF_INET;td.server.sin_port = htons(serverport);td.server.sin_addr.s_addr = inet_addr(serverip.c_str());td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (td.sockfd < 0){std::cout << "socker error" << endl;return 1;}td.serverip = serverip;// client 要bind吗?要!只不过不需要用户显式的bind!一般由OS自由随机选择!// 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!// 其实client的port是多少,其实不重要,只要保证主机上的唯一性就可以!// 系统什么时候给我bind呢?首次发送数据的时候pthread_t recvr, sender;pthread_create(&recvr, nullptr, recv_message, &td);pthread_create(&sender, nullptr, send_message, &td);pthread_join(recvr, nullptr);pthread_join(sender, nullptr);close(td.sockfd);return 0;
}

Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>void Usage(std::string proc)
{std::cout << "\n\tUsage:" << proc << "port[1024]\n" << std::endl;
}// ./udpserver port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);exit(0);}uint16_t port = std::stoi(argv[1]);std::unique_ptr<UdpServer> svr(new UdpServer(port));svr->Init(/**/);svr->Run();return 0;
}

makefile

.PHONY:all
all:udpserver udpclientudpserver:Main.ccg++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc g++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f udpserver udpclient

这里实现了一个小型的聊天室

所有人发的聊天消息都会显示在客户端。

每有一位新用户,服务器就会显示添加信息。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#,图论与图算法,图(Graph)的数据结构设计与源代码

因为后面即将发布的大量有关“图”的算法与源代码都需要用到下面的这些基础数据&#xff0c;为避免大家去下载&#xff0c;特意先发布于此。 一、图&#xff08;Graph&#xff09;的基础知识 图&#xff08;Graph&#xff09;是一组对象的图示&#xff0c;其中一些对象对通过链…

政安晨:【深度学习处理实践】(九)—— Transformer架构

咱们接着这个系列的上一篇文章继续&#xff1a; 政安晨&#xff1a;【深度学习处理实践】&#xff08;八&#xff09;—— 表示单词组的两种方法&#xff1a;集合和序列https://blog.csdn.net/snowdenkeke/article/details/136762323 Transformer是一种架构&#xff0c;用于在…

hot100 -- 矩阵

&#x1f442; Peter Pan - kidult. - 单曲 - 网易云音乐 &#x1f442; Bibliothque&#xff08;图书馆&#xff09; - Jasing Rye - 单曲 - 网易云音乐 目录 &#x1f33c;前言 &#x1f33c;二分模板 &#x1f382;矩阵置零 AC 标记数组 AC 标记变量 &#x1f6a9;…

网络安全等级测评师考试培训可以参考哪些资料?

网络安全是国家安全的重要组成部分&#xff0c;也是企业安全的重中之重&#xff1b;而网络安全等级测评师则是守护这一安全领域的重要力量。所以专业的网络安全等级测评师是非常重要。作为专业的网络安全等保测评师&#xff0c;他们肩负着对信息系统进行安全评估、发现潜在风险…

【01】htmlcssgit

01-前端干货-html&css 防脱发神器 一图胜千言 使用border-box控制尺寸更加直观,因此,很多网站都会加入下面的代码 * {margin: 0;padding: 0;box-sizing: border-box; }颜色的 alpha 通道 颜色的 alpha 通道标识了色彩的透明度,它是一个 0~1 之间的取值,0 标识完全…

Github Copilot 工具,无需账号,一键激活

① 无需账号&#xff0c;100%认证成功&#xff01;0风险&#xff0c;可联网可更新&#xff0c;&#xff0c;支持copilot版本升级&#xff0c;支持chat ② 支持windows、mac、linux系统等设备 ③一号通用&#xff0c;支持所有IDE(AppCode,CLion,DataGrip,GoLand,IntelliJ IDEA …

校园闲置物品交易网站 |基于springboot框架+ Mysql+Java+Tomcat的校园闲置物品交易网站设计与实现(可运行源码+设计文档)

推荐阅读100套最新项目 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 目录 前台功能效果图 用户功能模块 管理员功能登录前台功能效果图 系统功能设计 数据库E-R图设计 lunwen…

EXCEL+PYTHON学习3

1&#xff09; 遍历一个SHEET&#xff0c;无非就是两个循环&#xff0c;rows属性是取得所有行。 fn data3_16.xlsx wb openpyxl.load_workbook(fn) ws wb.active for row in ws.rows:for cell in row:print(cell.value, end )print() 2&#xff09; 返回工作表的最小行数…

【C语言】函数栈帧---函数的创建于销毁过程剖析(一览无遗)

目录 前言&#xff1a; 1.铺垫 寄存器 main函数被谁调用 2.正题 结论&#xff1a; 前言&#xff1a; 学习这么久以来&#xff0c;可能有很多疑问&#xff1a;局部变量怎么创建的&#xff1f;为什么局部变量的值是随机的&#xff1f;函数是怎么传参的&#xff1f;传参的顺…

2024最新手赚手机软件APP下载排行网站源码及应用商店源码

这是一款简洁蓝色的手机软件下载应用排行、平台和最新发布网站&#xff0c;采用响应式织梦模板。主要包括主页、APP列表页、APP详情介绍页、新闻资讯列表、新闻详情页、关于我们等模块页面。 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/88898956 更…

java serlvet 高校学生画像平台系统Myeclipse开发mysql数据库web结构java编程计算机网页项目echarts图形展现

一、源码特点 java serlvet 高校学生画像平台系统是一套完善的java web信息管理系统 系统采用serlvetdaobean 模式开发本系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCA…

yocto 集成ros2(基于raspberrypi 4B)

yocto 集成ros2 上一小节已经使用yocto编译出了raspberrypi 4B的image。并且成功刷机并且启动登陆&#xff1a; 链接: yocto 编译raspberrypi 4B并启动 本节我们将ros2机器人操作系统移植到我们的yocto系统里面。 1. 下载ros layer 上小节我们编译的yocto image是基于kirkst…