大名县建设局网站,琳琅秀网站建设,室内设计和装修设计,威海网站推广#x1f941;作者#xff1a; 华丞臧. #x1f4d5;专栏#xff1a;【网络】 各位读者老爷如果觉得博主写的不错#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方#xff0c;欢迎在评论区指出。 推荐一款刷题网站 #x1f449; LeetCode刷题网站 文章… 作者 华丞臧. 专栏【网络】 各位读者老爷如果觉得博主写的不错请诸位多多支持(点赞收藏关注)。如果有错误的地方欢迎在评论区指出。 推荐一款刷题网站 LeetCode刷题网站 文章目录一、网络编程套接字1.1 认识端口号1.2 认识UDP和TCP协议1.3 网络字节序二、UDP套接字2.1 sockaddr结构2.2 简单的UDP网络程序2.1 socket 常见APIrecvformbindinet_addr2.1 封装 UdpSocketserverinitstart2.2 udpclient2.4 日志显示2.5 测试一、网络编程套接字
在数据包的头部中有两个IP地址分别叫做源IP地址和目的IP地址但是光有这两个IP地址不能实现网路通信在计算机上进行通信的时候实际上是用户和用户在进行网络通信而用户通常是用程序体现的也就是说用户是通过计算机上的某一个进程软件来进行数据的交互。 结论网络通信的本质就是进程间的通信。 因此在实现网络通信时不仅需要IP地址来确保主机的唯一性还需要一个标记来确保主机上进程的唯一性。我们将这个表示主机上进程唯一性的标记称为端口号。
1.1 认识端口号
端口号(port)是传输层协议的内容。
端口号是一个2字节16位的整数。端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。IP地址 端口号能够标识网络上的某一台主机的某一个进程。一个端口号只能被一个进程占用。端口号可以将操作系统的进程管理和网络解耦。进程不一定都需要网络通信端口号可以标识需要进行网络通信的进程。0~1023的端口号是已经被使用了的用户的端口号只能从1024 ~ 65536。
理解端口号和进程PID
PID表示唯一一个进程端口号也表示唯一一个进程一个进程可以绑定多个端口号一个端口号只能绑定唯一一个进程。
1.2 认识UDP和TCP协议
传输层协议(TCP和UDP)的数据段中有两个端口号分别叫做源端口号和目的端口号就是在描述 “数据是谁发的, 要发给谁”。
对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识此处先了解TCP
传输层协议有连接可靠传输面向字节流
对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识此处先了解UDP
传输层协议无连接不可靠传输面向数据报
UDP实现足够简单TCP实现较为复杂两个协议各有优缺点不同场景选择合适的协议即可。
1.3 网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分 那么如何定义网络数据流的地址呢? 两台主机进行网络通信发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出接收主机把从网络上接到的字节依次保存在接收缓冲区中也是按内存地址从低到高的顺序保存因此网络数据流的地址应这样规定先发出的数据是低地址后发出的数据是高地址。 世界各地的计算机生产厂商非常多 因此对于发送方和接收方的大小端字节序是无法确定的。无法控制通信双方主机但是我们可以规定网络中的字节序TCP/IP协议规定网络数据流应采用大端字节序即低地址高字节。不管这台主机是大端机还是小端机都会按照这个TCP/IP规定的网络字节序来发送/接收数据如果当前发送主机是小端就需要先将数据转成大端否则就忽略, 直接发送即可。 为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络 字节序和主机字节序的转换。 这些函数名很好记h表示hostn表示networkl表示32位长整数s表示16位短整数。例如htonl表示将32位的长整数从主机字节序转换为网络字节序例如将IP地址转换后准备发送。如果主机是小端字节序这些函数将参数做相应的大小端转换然后返回。如果主机是大端字节序这些函数不做转换将参数原封不动地返回。
二、UDP套接字
2.1 sockaddr结构
socket API是一层抽象的网络编程接口适用于各种底层网络协议如IPv4、IPv6以及后面要讲的UNIX Domain Socket。然而各种网络协议的地址格式并不相同。用于本地通信和用于网络通信的套接字接口需要的数据都是不同的为了能够使用统一的接口来实现网络通信和本地通信接口设计者给出了一个 抽象结构如下图 系统中存在三种结构体struct sockaddr抽象类型、struct sockaddr_inINET网路套接字、struct sockaddr_unUnix域套接字套接字。struct sockaddr内部会处理判断数据的前16位进行类型识别然后强转成对应的类型实现切片可以理解为C的中多态。 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 结构
sockaddr_in 结构 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
in_addr结构 in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数。
2.2 简单的UDP网络程序
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);说明 domain域进行本地通信还是网络通信。 AF_UNIX本地通信AF_INETIPV4 type套接字类型决定通信对应的报文类型常用的为流式和用户数据报。 protocol协议类型网络应用中直接设为0即可。 socket套接字返回的是文件描述符可以理解所有网络操作最终都是文件描述符级的操作。
recvform
用于从套接字文件描述符中读取数据。 #include sys/types.h#include sys/socket.hssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);说明
sockfd套接字buf用于接收数据的指针从sockfd文件中读取的数据放入指针指向的空间中len读取数据的字节数flags模式src_addr用于接收读取对方的port和ip地址addrlensrc_addr数据的字节大小
bind
用于绑定网络信息指明ip和port。
#include sys/types.h
#include sys/socket.hint bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);sockfd套接字文件描述符addr需要绑定的sockaddr结构addrlen对应sockaddr结构的字节大小
inet_addr
用于将字符串中的点分十进制转换成32位4字节的表示形式。
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.hint inet_aton(const char *cp, struct in_addr *inp);in_addr_t inet_addr(const char *cp);in_addr_t inet_network(const char *cp);char *inet_ntoa(struct in_addr in);struct in_addr inet_makeaddr(int net, int host);in_addr_t inet_lnaof(struct in_addr in);in_addr_t inet_netof(struct in_addr in);2.1 封装 UdpSocket
server
server是提供服务的一端可以理解为服务器用来接收用户传输的消息同时也可以给用户发消息可以将服务器封装成一个类类当中包含套接字的初始化以及提供服务的接口启动服务器时必须将端口号以参数的形式传递给main函数ip地址可传可不传。
#include iostream
#include string.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include unistd.h#include Log.hppstatic void Usage(const std::string porc)
{std::cout Usage:\n\t porc port [ip] std::endl;
}// 写一个简单的udpSever
// 云服务器有一些特殊情况
// 1. 禁止你bind云服务器上的任何确定IP 只能使用INADDR_ANY如果你是虚拟机随意class UdpServer
{
public:UdpServer(int port, std::string ip ):sockfd_(-1),port_(port),ip_(ip){}~UdpServer(){}
public:void init() //初识化server{} void start() //server开始服务{}
private:// 服务器的fdint sockfd_;// 服务器必须得有端口号信息uint16_t port_;// 服务器必须得有IP地址std::string ip_;
};// ./server port ip
int main(int argc, char *argv[])
{if(argc ! 2 argc ! 3) {Usage(argv[0]);exit(PARA_ERR);}uint16_t port atoi(argv[1]); std::string ip;if(argc 3){ip argv[2];}UdpServer server(port,ip);server.init(); // 创建并配置套接字server.start(); // 提供服务return 0;
}init
初识化的任务主要是创建套接字将套接字绑定网络信息指明ip和port。一般初识化分为以下几个步骤
void init(){// 1. 创建套接字// domain -- 域// type -- 套接字类型SOCK_DGRAM--数据报格式// protocol -- 协议类型网络应用中0// 返回值其实是文件描述符sockfd_ socket(AF_INET, SOCK_DGRAM, 0); //就是打开一个文件if(sockfd_ 0){logMessage(FATAL, socket:%s:%d, strerror(errno), sockfd_);exit(SOCK_ERR);}logMessage(DEBUG, socket create success:%d, sockfd_);// 2. 绑定网络信息指明ipport// 2.1 先填充基本信息到 struct sockaddr_instruct sockaddr_in local;bzero(local, sizeof(local)); //初始化为0local.sin_family AF_INET; //填充协议家族域local.sin_port htons(port_); //填充服务对应的端口号信息,一定会发给对方因此port_一定回到网络中//local.sin_addr;local.sin_addr.s_addr ip_.empty()? htonl(INADDR_ANY) : inet_addr(ip_.c_str()); // htonl将INADDR_ANY转换成32位网络字节序 inet_addr将字符串中的点分十进制转换成32位比特位if(bind(sockfd_, (const struct sockaddr*)local, sizeof(local)) -1){logMessage(FATAL, bind:%s, strerror(errno));exit(BIND_ERR);}logMessage(DEBUG, socket bind success:%d, sockfd_);//完成}start
start()是server提供服务的接口因此该函数必须是一个死循环服务器都是在一个死循环当中以给用户提供持续的服务在start函数中主要完成接收用户发送的消息并且将消息提取出来其主要步骤如下图
void start()
{// 服务器都是在一个死循环当中char inbuffer[1024]; //将来读取到的数据都放在这里char outbuffer[1024]; //将来发送的数据都放在这里while(true){struct sockaddr_in peer; //输出型参数socklen_t len sizeof(peer); //输入输出型参数// UDP是无连接的// 对方发消息你需要接收消息ssize_t s recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, \(struct sockaddr*)peer, len);logMessage(DEBUG, server 提供服务中.....);if(s 0) {//接收成功inbuffer[s] \0;}else if(s -1){//logMessage(WARINING, recvfrom fialed:%s[%d], strerror(errno), sockfd_);continue;}// 读取成功的,除了读取到对方的数据你还要读取到对方的网络地址[ip:port]std::string peerIp inet_ntoa(peer.sin_addr); //拿到对方的ipuint32_t peerPort ntohs(peer.sin_port); //拿到对方的port// 打印出来客户端给服务器发送过来的消息logMessage(NOTICE, [%s:%d]# %s, peerIp.c_str(), peerPort, inbuffer);logMessage(DEBUG, server 提供服务中.....);//sleep(1);}
}2.2 udpclient
客户端用来连接服务器并且使用服务器提供的服务用户想要客户端与服务器进行网络通信就必须将服务器使用的端口号和ip传给客户端客户端通过端口号和ip可以实现与服务器地网络通信。
#include iostream
#include sys/types.h
#include sys/socket.h
#include Log.hpp
#include netinet/in.h
#include arpa/inet.h
#include unistd.hstatic void Usage(std::string name)
{std::cout Usage:\n\t name server_ip server_port std::endl;
} // ./udpClient IP port
int main(int argc, char* argv[])
{if(argc ! 3) //必须等于3必须传ip和port{Usage(argv[0]);exit(PARA_ERR);}std::string ip argv[1];uint16_t port atoi(argv[2]);// 2. 创建客户端// 2.1 创建socketint sockfd socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd 0);struct sockaddr_in ser;bzero(ser, sizeof ser);ser.sin_family AF_INET; //填充与服务器相同的协议ser.sin_port htons(port); //服务器的端口号ser.sin_addr.s_addr inet_addr(ip.c_str()); //服务器ipstd::string buffer;while(true){std::cerr Please Enter# ;std::getline(std::cin, buffer);// 发送消息给serversendto(sockfd, buffer.c_str(), buffer.size(), 0,(const struct sockaddr *)ser, sizeof(ser)); // 首次调用sendto函数的时候我们的client会自动bind自己的ip和port}close(sockfd);return 0;
}2.4 日志显示 vs_start使用距离可变参数最近的一个参数初始化apva_arg对可变参数列表提参提参类型为typeva_end将ap指针置为空
//将可变参数列表全部转化为某个字符串当中
int vsnprintf(char *str, size_t size, const char *format, va_list ap);说明str用于保存数据的字符串数组地址size最大写入的空间大小format以什么格式化写入ap可变参数列表部分#pragma once
#include cstdio
#include ctime
#include cstdarg
#include cassert
#include cstring
#include cerrno
#include stdlib.h#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3
#define SOCK_ERR 4
#define BIND_ERR 5
#define PARA_ERR 6const char *log_level[]{DEBUG, NOTICE, WARINING, FATAL};// logMessage(DEBUG, %d, 10);
void logMessage(int level, const char *format, ...)
{assert(level DEBUG);assert(level FATAL);char *name getenv(USER); //获取用户名char logInfo[1024];va_list ap; // ap - char* //va_start(ap, format); //vsnprintf(logInfo, sizeof(logInfo)-1, format, ap); //写入logInfova_end(ap); // ap NULLFILE *out (level FATAL) ? stderr:stdout;fprintf(out, %s | %u | %s | %s\n, \log_level[level], \(unsigned int)time(nullptr),\name nullptr ? unknow:name,\logInfo);
}2.5 测试
使用127.0.0.1可以进行本地通信。 使用云服务器的ip地址可以进行网络通信。