网络编程套接字----UDP协议
创始人
2024-05-03 04:58:33
0

文章目录

  • 前言
  • 一、理解源IP地址和目的IP地址
  • 二、认识端口号
    • 理解"端口号"和"进程ID"
    • 理解源端口号和目的端口号
  • 三、认识TCP协议
  • 四、认识UDP协议
  • 五、网络字节序
  • 六、socket编程接口
    • socket常见API
    • sockaddr结构
    • sockaddr结构
    • sockaddr_in 结构
    • in_addr结构
  • 七、地址转换函数
    • 关于inet_ntoa
  • 八、简单的UDP网络程序
    • 封装UDPSocket
    • Windows下的客户端实现
  • 总结


前言

  1. 认识IP地址,端口号,网络字节序等网络编程中的基本概念;
  2. 学习socket api的基本用法;
  3. 能够实现一个简单的UDP客户端/服务器;
  4. 能够实现一个简单的TCP客户端/服务器(单连接版本,多进程版本,多线程版本);
  5. 理解TCP服务器建立连接,发送数据,断开连接的流程.

正文开始!

一、理解源IP地址和目的IP地址

  • 源IP地址:对应的就是标定通信主机的本机主机.
  • 目的IP地址:对应的就是标定通信主机的目的主机.

二、认识端口号

在这里插入图片描述

端口号(port)是传输层协议的内容:

  • 端口号是一个2字节16位的整数;
  • 端口号用来表示一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理;
  • IP地址+端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用;

理解"端口号"和"进程ID"

之前在进程的学习中可以知道pid表示唯一一个进程;此处端口号也是表示一个进程,那么这两者有什么关系呢?

一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定.

理解源端口号和目的端口号

传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号.就是在描述"数据是谁发的,要发给谁";
在这里插入图片描述

三、认识TCP协议

在这里我们先对TCP(Transmission Control Protocol 传输控制协议)做一个简单的认识;

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

四、认识UDP协议

在这我们也是对UDP(User Dategram Protocol 用户数据报协议)做一个简单的认识;

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

五、网络字节序

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

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

在这里插入图片描述

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

在这里插入图片描述

  • 这些函数名很好记,h表示host,n表示network,l表示32为长整数,s表示16位短整数.
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送.
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些函数不做转换,将参数原封不动的返回.

六、socket编程接口

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

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6.
然后各种不同网络协议的地址格式并不相同.
在这里插入图片描述

  • IPv和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以及各种类型的sockaddr结构体指针作为参数;

sockaddr结构

在这里插入图片描述

sockaddr_in 结构

在这里插入图片描述
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息,地址类型,端口号,IP地址.

in_addr结构

在这里插入图片描述
in_addr用来表示一个IPv4的IP地址.其实就是一个32位整数.

七、地址转换函数

基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr,sin_addr表示32位的IP地址.但是我们通常使用点分十进制的字符串表示IP地址,以下的函数可以在字符串表示和in_addr表示之间转换:

在这里插入图片描述

关于inet_ntoa

inet_ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果,那么是否需要调用者手动释放呢?
在这里我们查看man手册
在这里插入图片描述

man手册上面说,inet_ntoa函数是把这个结果放到了静态存储区.这个时候不需要我们手动释放.

那么问题来了,如果我们多次调用这个函数,会有什么样的效果呢?

参考如下代码

#include
#include
#includeint main()
{struct sockaddr_in addr1;struct sockaddr_in addr2;addr1.sin_addr.s_addr=0;addr2.sin_addr.s_addr=0xffffffff;char* ptr1 = inet_ntoa(addr1.sin_addr);char* ptr2 = inet_ntoa(addr2.sin_addr);printf("ptr1: %s,ptr2: %s\n",ptr1,ptr2);return 0;
}

在这里插入图片描述
因为inet_ntoa把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果.

思考:如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?—>一定会的!
在APUE这本书中,明确提出 inet_ntoa 不是线程安全的函数;
在多线程的环境下,推荐使用 inet_ntop,这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题;

八、简单的UDP网络程序

实现一个大小写转化的功能
在这里插入图片描述

封装UDPSocket

makefile

.PHONY:all
all:udpClient udpServer
udpClient:udpClient.ccg++ -o $@ $^ -std=c++11 -lpthread
udpServer:udpServer.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -rf udpClient udpServer

Log.hpp

#pragma once
#include
#include
#include
#include
#include
#include
#include
#define DEBUG     0
#define NOTICE    1
#define WARINING  2
#define FATAL     3const 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 logInfo[1024];char* name=getenv("USER");va_list ap; //ap--->char*va_start(ap,format);vsnprintf(logInfo,sizeof(logInfo)-1,format,ap);va_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);
}// char* s=format;
// while(s)
// {
//     case: '%'
//     if(*(s+1)=='d')int x=va_arg(ap,int);
//     break;
// }

udpClient.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;struct sockaddr_in server;static void Usage(string proc)
{printf("Usage\n\t%s server_ip server_port\n", proc.c_str());
}
void *recverAndPrint(void *args)
{while (true){int sockfd = *(int *)args;char buffers[1024];memset(buffers,0,sizeof buffers);struct sockaddr_in temp;socklen_t len = sizeof(temp);ssize_t s = recvfrom(sockfd, buffers, sizeof(buffers) - 1, 0, (struct sockaddr *)&temp, &len);if (s > 0){buffers[s] = 0;cout << "server echo# " << buffers << endl;}}
}
// ./udpClient server_ip server_port
//如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{if (argc != 3){Usage(argv[0]);exit(1);}// 1.根据命令行设置要访问的服务器IPstring server_ip = argv[1];uint16_t server_port = stoi(argv[2]);// 2.创建客户端// 2.1创建socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);// 2.2 client 需不需要bind???--->需要bind,但是不需要用户自己bind,而是OS自动给你bind//  所谓的"不需要",指的是::不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!//如果我非要自己bind呢?  可以!  严重不推荐!//  所有的客户端软件 <->服务器 通信的时候,必须得有 client[ip,port] <->server[ip,port]//  为什么呢? client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法使用了//  那么server凭什么要bind呢?server提供的服务,必须要被所有人知道!server不能随便改变!// 2.2填写服务器对应的信息bzero(&server, sizeof server);socklen_t len = sizeof(server);server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());pthread_t t;pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);// 3.通讯过程string buffer;while (true){cerr << "Please Enter# ";getline(cin, buffer);//发送消息给serversendto(sockfd, buffer.c_str(),buffer.size(), 0, (const struct sockaddr *)&server, len);//首次调用sendto函数的时候,我们client会自动bind自己的ip和port}close(sockfd);return 0;
}

udpServer.cc

#include 
#include
#include 
#include 
#include 
#include 
#include 
#include 
#include "log.hpp"using namespace std;//我们想写一个简单的udpServer
//云服务器有一些特殊情况
// 1.禁止你bind云服务器上的任何确定IP,只能使用INADDR_ANY,如果是虚拟机的话就可以!
class UdpServer
{
public:UdpServer(uint16_t port, string ip = ""): _sockfd(-1), _port(port), _ip(ip){}~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);}logMessage(DEBUG, "socket create success: %d", _sockfd);// 2.绑定网络信息,知名ip+port// 2.1先填充基本信息到 struct sockaddr_instruct sockaddr_in local;    // local在用户栈上开辟的空间--->临时变量--->写入内核中bzero(&local, sizeof local); // memsetlocal.sin_family = AF_INET;  // 填充协议家族,域//填充服务器对应得端口号信息,一定是会发给对方的,_port一定回到网络中的local.sin_port = htons(_port);//服务器都必须具有IP地址,"81.70.251.220",字符串风格的点分十进制-->四字节IP-->uint32_t ip// INADDR_ANY(0):程序员不关心会bind到哪一个ip,任意地址bind,强烈推荐的做法,所有服务器一般的做法// inet_addr:指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行h->nlocal.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) : inet_addr(_ip.c_str());// 2.2if (bind(_sockfd, (const struct sockaddr *)&local, sizeof local) < 0){logMessage(FATAL, "bind:%s:%d", strerror(errno), _sockfd);exit(2);}logMessage(DEBUG, "socket bind success: %d", _sockfd);// done}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);if (s > 0){inbuffer[s] = '\0'; //当做字符串}else if (s == -1){logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), _sockfd);continue;}//读取成功,除了读取到对方的数据,你还要读取到对方的网络地址[ip,port]string peerIp=inet_ntoa(peer.sin_addr);//拿到了对方的ipuint32_t peerPort=ntohs(peer.sin_port);//拿到了对方的portcheckOnlineUser(peerIp,peerPort,peer);//如果存在,什么都不做,如果不存在就添加//打印出客户端给服务器发送的消息logMessage(NOTICE, "[%s|%d]# %s",peerIp.c_str(),peerPort,inbuffer);for(int i=0;iif(isalpha(inbuffer[i])&&islower(inbuffer[i]))outbuffer[i]=toupper(inbuffer[i]);elseoutbuffer[i]=inbuffer[i];}messageRoutine(peerIp,peerPort,inbuffer);//消息路由//线程池!//sendto(_sockfd,outbuffer,strlen(outbuffer),0,(const struct sockaddr*)&peer,sizeof(peer));// logMessage(NOTICE,"server provide service succsee...");// sleep(1);}}void checkOnlineUser(string& ip,uint32_t port,struct sockaddr_in& peer){string key=ip;key+=":";key+=to_string(port);auto iter =_users.find(key);if(iter==_users.end()){_users.insert({key,peer});}else{//do nothing}}void messageRoutine(string ip,uint32_t port,string info){string message="[";message+=ip;message+=":";message+=to_string(port);message+="]";message+=info;for(auto& e:_users){sendto(_sockfd,message.c_str(),message.size(),0,(const struct sockaddr*)&(e.second),sizeof(e.second));}}
private://服务器socket fd信息int _sockfd;// 服务器必须得有端口号信息uint16_t _port;//服务器必须要有ip地址string _ip;//onlineuserstd::unordered_map _users;
};
// struct ip
// {
//     uint32_t part1:8;
//     uint32_t part2:8;
//     uint32_t part3:8;
//     uint32_t part4:8;
// };static void Usage(const string proc)
{printf("Usage:\n\t%s port [ip]\n", proc.c_str());
}// ./udpServer port [ip]
int main(int argc, char *argv[])
{if (argc != 2 && argc != 3){Usage(argv[0]);exit(1);}uint16_t port = stoi(argv[1]);string ip;if (argc == 3){ip = argv[2];}UdpServer svr(port, ip);svr.init();svr.start();return 0;
}

在这里插入图片描述

Windows下的客户端实现

#pragma warning(disable:4996)
#pragma comment(lib,"Ws2_32.lib")
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{WSADATA data;WSAStartup(MAKEWORD(2,2),&data);string server_ip = "81.70.251.220";uint16_t server_port = 8081;// 2.创建客户端// 2.1创建socketSOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);// 2.2 client 需不需要bind???--->需要bind,但是不需要用户自己bind,而是OS自动给你bind//  所谓的"不需要",指的是::不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!//如果我非要自己bind呢?  可以!  严重不推荐!//  所有的客户端软件 <->服务器 通信的时候,必须得有 client[ip,port] <->server[ip,port]//  为什么呢? client很多,不能给客户端bind指定的port,port可能被别的client使用了,你的client就无法使用了//  那么server凭什么要bind呢?server提供的服务,必须要被所有人知道!server不能随便改变!// 2.2填写服务器对应的信息struct sockaddr_in server;memset(&server,'\0',sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);server.sin_addr.s_addr = inet_addr(server_ip.c_str());// 3.通讯过程string buffer;while (true){cerr << "Please Enter# ";getline(cin, buffer);//发送消息给serversendto(sockfd, buffer.c_str(), \buffer.size(), 0, (const struct sockaddr*)&server, sizeof(server));//首次调用sendto函数的时候,我们client会自动bind自己的ip和port} closesocket(sockfd);WSACleanup();return 0;
}

在这里插入图片描述

中间的乱码问题可能是因为编码问题,但是其他的内容我们就可以直接的进行通讯.


总结

(本章完!)

相关内容

热门资讯

开源电脑安卓系统排行,探索自由... 亲爱的电脑爱好者们,你是否曾想过,在电脑的世界里,也能体验到安卓系统的便捷与乐趣?没错,这就是今天我...
如何清空相册安卓系统,轻松恢复... 手机里的相册是不是越来越满,看着那些堆积如山的照片,是不是有点头疼呢?别急,今天就来教你怎么在安卓系...
安卓系统要停止更新,拥抱新变革 你知道吗?最近有个大消息在安卓圈里炸开了锅!安卓系统,这个陪伴我们多年的老朋友,竟然要停止更新了!这...
安卓系统怎样强行关机,安卓系统... 手机突然卡壳了,是不是又想强行关机了?别急,今天就来教你安卓系统怎样强行关机,让你轻松应对各种突发状...
安卓系统如何删除桌面,轻松删除... 手机桌面乱糟糟的,是不是感觉像你的房间一样,东西堆得有点多?别急,今天就来教你怎么给安卓系统的桌面来...
安卓系统怎么发英语,Andro... 你有没有想过,在安卓系统上发送英语信息竟然也能变得如此简单有趣?没错,就是那种轻松自如,仿佛英语是你...
最早期的安卓系统,揭秘最早期安... 亲爱的读者,你是否曾好奇过,那个陪伴我们手机成长的安卓系统,它的起源究竟是怎样的呢?今天,就让我们一...
安卓双系统添加应用,轻松实现多... 你有没有想过,你的安卓手机里可以同时运行两个系统呢?听起来是不是很酷?想象一边是熟悉的安卓系统,一边...
pipo安卓进系统慢,探究pi... 最近是不是发现你的Pipo安卓系统更新或者运行起来特别慢?别急,今天就来给你好好分析分析这个问题,让...
怎样使用安卓手机系统,安卓手机... 你有没有发现,安卓手机已经成为我们生活中不可或缺的一部分呢?从早晨闹钟响起,到晚上睡前刷剧,安卓手机...
双系统安卓安装caj,轻松实现... 你有没有想过,你的安卓手机里装上双系统,是不是就能同时享受安卓和Windows系统的乐趣呢?没错,这...
安卓使用ios系统教程,安卓用... 你是不是也和我一样,对安卓手机上的iOS系统充满了好奇?想要体验一下苹果的优雅和流畅?别急,今天我就...
安卓系统gps快速定位,畅享便... 你有没有遇到过这样的情况:手机里装了各种地图导航软件,但每次出门前都要等上好几分钟才能定位成功,急得...
安卓手机系统更新原理,原理与流... 你有没有发现,你的安卓手机最近是不是总在提醒你更新系统呢?别急,别急,让我来给你揭秘一下安卓手机系统...
安卓系统通知管理,全面解析与优... 你有没有发现,手机里的通知就像是一群调皮的小精灵,时不时地跳出来和你互动?没错,说的就是安卓系统的通...
安卓系统手机哪买,揭秘哪里购买... 你有没有想过,拥有一部安卓系统手机是多么酷的事情呢?想象你可以自由安装各种应用,不受限制地探索各种功...
安卓系统 ipv4,基于安卓系... 你知道吗?在智能手机的世界里,有一个系统可是无人不知、无人不晓,那就是安卓系统。而在这个庞大的安卓家...
目前安卓是什么系统,探索安卓系... 亲爱的读者,你是否曾好奇过,如今安卓系统究竟是什么模样?在这个科技飞速发展的时代,操作系统如同人体的...
安卓6.0系统比5.0,从5.... 你有没有发现,自从手机更新了安卓6.0系统,感觉整个人都清爽了不少呢?没错,今天咱们就来聊聊这个话题...
安卓2.36系统升级,功能革新... 你知道吗?最近安卓系统又来了一次大变身,那就是安卓2.36系统升级!这可不是一个小打小闹的更新,而是...