1. 预备知识
1.1 ip地址
-
要想使网络中的计算机能够进行通信,必须为每台计算机指定一个标识号,通过这个标识号来指定接受数据的计算机或者发送数据的计算机。
在TCP/IP协议中,这个标识号就是IP地址,它可以唯一标识一台计算机
。 -
目前,IP地址广泛使用的版本是IPv4,它是由4个字节大小的二进制数来表示,如:00001010000000000000000000000001。由于二进制形式表示的IP地址非常不便记忆和处理,因此通常会将IP地址写成十进制的形式,每个字节用一个十进制数字(0-255)表示,数字间用符号“.”分开,如 “192.168.1.100”。
-
源ip地址
就是发出这个数据包的电脑的ip地址,它是数据的来源。 -
目标ip地址
就是数据最终要到达的那台电脑的ip地址。 -
路由器就是根据目标ip地址确定如何转发数据包,最终把数据以最佳路由发送到目标主机的。
1.2 端口号
- 通过IP地址可以连接到指定计算机,但如果想
访问目标计算机中的某个应用程序,还需要指定端口号。
在计算机中,不同的应用程序是通过端口号区分的。 - 端口号是用两个字节(16位的二进制数)表示的,它的取值范围是065535,其中,01023之间的端口号用于一些知名的网络服务和应用,用户的普通应用程序需要使用1024以上的端口号,从而避免端口号被另外一个应用或服务所占用。
源端口
就是本机程序用来发送数据的端口。目的端口
就是对方主机用哪个端口接收。- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
- 一个端口号只能被一个进程占用。
注意
:一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
关系图:
2. 网络字节序
网络字节序定义
:收到的第一个字节被当作高位看待,这就要求发送端发送的第一个字节应当是高位。而在发送端发送数据时,发送的第一个字节是该数字在内存中起始地址对应的字节。可见多字节数值在发送前,在内存中数值应该以大端法存放。- 网络协议指定了**
通讯字节序:大端
**。只有在多字节数据处理时才需要考虑字节序,运行在同一台计算机上的进程相互通信时,一般不用考虑字节序,异构计算机之间通讯,需要转换自己的字节序为网络字节。主机字节序是小端,所以才需要进行字节序转换。
2.1 大端模式和小端模式
字节序是指多字节数据的存储顺序,在设计计算机系统的时候,有两种处理内存中数据的方法:大端格式、小端格式。
- 小端格式(Little-Endian):将低位字节数据存储在低地址;
- 大端格式(Big-Endian):将高位字节数据存储在低地址。
判断是否是小端:
bool isLittleEndian()
{
union U
{
int i;
char c;
};
U u;
u.i = 0x12345678;
return u.c == 0x78;
}
2.2 字节序转换函数
#include <arpa/inet.h>
// 将 32位主机字节序数据转换成网络字节序数据
//(h:host, n:net,l:long)
uint32_t htonl(uint32_t hostint32);
// 将 16 位主机字节序数据转换成网络字节序数据
uint16_t htons(uint16_t hostint16);
// 将 32 位网络字节序数据转换成主机字节序数据
uint32_t ntohl(uint32_t netint32);
// 将 16 位网络字节序数据转换成主机字节序数据
uint16_t ntohs(uint16_t netint16);
3. sockaddr结构
struct sockaddr很多网络编程API诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是其他的,由地址族确定,然后函数内部再强制转化为所需的地址类型。
结构图
:
struct sockaddr {
unsigned short sa_family; // 2 bytes address family, AF_xxx unsiged short
char sa_data[14]; // 14 bytes of protocol address
};
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
用例
:
4. 认识UDP协议
- Internet 协议集支持一个无连接的传输协议,该协议称为用户数据报协议(UDP,User Datagram Protocol)。UDP 为应用程序提供了一种
无需建立连接就可以发送封装的 IP 数据包的方法
。 - Internet 的
传输层
有两个主要协议,互为补充。无连接的是 UDP,它除了给应用程序发送数据包功能并允许它们在所需的层次上架构自己的协议之外,几乎没有做什么特别的事情。面向连接的是 TCP,该协议几乎做了所有的事情。 传输层协议
无连接
不可靠传输
面向数据报(SOCK_DGRAM)
UDP的框架图
:
5. socket编程接口
- 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
用例
:
- domain:用于设置网络通信的域,socket根据这个参数选择信息协议的族(一般选择如下两个,但不止如此两个)
AF_INET
IPv4 Internet protocols //用于IPV4AF_INET6
IPv6 Internet protocols //用于IPV6
- type:(只列出最重要的三个):
SOCK_STREAM
Provides sequenced, reliable, two-way, connection-based byte streams. //用于TCPSOCK_DGRAM
Supports datagrams (connectionless, unreliable messages ). //用于UDPSOCK_RAW
Provides raw network protocol access. //RAW类型,用于提供原始网络访问
- protocol:置0即可
- 返回值:
成功:非负的文件描述符
失败:-1
- 绑定端口号 (TCP/UDP, 服务器)
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
用例
:
- sockfd:正在监听端口的套接口文件描述符,通过socket获得
- my_addr:需要绑定的IP和端口
- addrlen:my_addr的结构体的大小
- 返回值:
成功:0
失败:-1
- 发送 (TCP/UDP, 服务器)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
用例
:
- sockfd:正在监听端口的套接口文件描述符,通过socket获得
- buf:发送缓冲区,往往是使用者定义的数组,该数组装有要发送的数据
- len:发送缓冲区的大小,单位是字节
- flags:填0即可
- dest_addr:指向接收数据的主机地址信息的结构体,也就是该参数指定数据要发送到哪个主机哪个进程
- addrlen:表示第五个参数所指向内容的长度
- 返回值:
成功:返回发送成功的数据长度
失败: -1
- 接收(TCP/UDP, 服务器)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
用例
:
- sockfd:正在监听端口的套接口文件描述符,通过socket获得
- buf:接收缓冲区,往往是使用者定义的数组,该数组装有接收到的数据
- len:接收缓冲区的大小,单位是字节
- flags:填0即可
- src_addr:指向发送数据的主机地址信息的结构体,也就是我们可以从该参数获取到数据是谁发出的
- addrlen:表示第五个参数所指向内容的长度
- 返回值:
成功:返回接收成功的数据长度
失败: -1
6. 代码实现UDP
- udpClient.cpp
#include"udpClient.hpp"
void Usage(std::string proc)
{
std::cout<<"Usage: "<<proc<<" svr_ip svr_port"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
udpClient uc(argv[1],atoi(argv[2]));
uc.initClient();
uc.start();
return 0;
}
- udpClient.hpp
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstdlib>
class udpClient
{
public:
//Server ip , port;
udpClient(std::string _ip="127.0.0.1",int _port=8080)
:ip(_ip)
,port(_port)
{
}
void initClient()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
std::cout<<"sock"<<sock<<std::endl;
// struct sockaddr_in local;
// local.sin_family = AF_INET;
// local.sin_port = htons(port);
// local.sin_addr.s_addr = inet_addr(ip.c_str());
//
//
// if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
// {
// std::cerr<<"bind error!\n"<<std::endl;
// exit(1);
// }
}
void start()
{
// char msg[64];
std::string msg;
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
peer.sin_addr.s_addr = inet_addr(ip.c_str());
while(1)
{
std::cout<<"Please Enter: ";
std::cin>>msg;
if(msg == "quit")
{
break;
}
sendto(sock,msg.c_str(),msg.size(),0,(struct sockaddr*)&peer,sizeof(peer));
char echo[128];
ssize_t s=recvfrom(sock,echo,sizeof(echo)-1,0,nullptr,nullptr);
echo[s]='\0';
if(s>0)
{
std::cout<<" server: "<<echo<<std::endl;
}
// msg[0]='\0';
// struct sockaddr_in end_point;
// socklen_t len = sizeof(end_point);
// ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
// if(s>0)
// {
// msg[s]='\0';
// std::cout<<"Client :" << msg <<std::endl;
// std::string echo_string = msg;
// echo_string += " [server echo!]";
// sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);
// }
}
}
~udpClient()
{
close(sock);
}
private:
std::string ip;
int port;
int sock;
};
- udpServer.cpp
#include"udpServer.hpp"
void Usage(std::string proc)
{
std::cout<<"Usage: "<< proc <<"local_ip local_proc"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
udpServer *up = new udpServer(atoi(argv[1]));
up->initServer();
up->start();
delete up;
}
- udpServer.hpp
#pragma once
#include<iostream>
#include<string>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<cstdlib>
#include<cstdio>
#include<map>
class udpServer
{
public:
udpServer(int _port=8080)
// :ip(_ip)
:port(_port)
{
dict.insert(std::pair<std::string,std::string>("apple","苹果"));
dict.insert(std::pair<std::string,std::string>("banana","香蕉"));
dict.insert(std::pair<std::string,std::string>("string","字符串"));
dict.insert(std::pair<std::string,std::string>("int","整形"));
dict.insert(std::pair<std::string,std::string>("float","浮点型"));
}
void initServer()
{
sock = socket(AF_INET,SOCK_DGRAM,0);
std::cout<<"sock"<<sock<<std::endl;
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cerr<<"bind error!\n"<<std::endl;
exit(1);
}
}
void start()
{
char msg[64];
while(1)
{
msg[0]='\0';
struct sockaddr_in end_point;
socklen_t len = sizeof(end_point);
ssize_t s=recvfrom(sock,msg,sizeof(msg)-1,0,(struct sockaddr*)&end_point,&len);
if(s>0)
{
char buff[16];
sprintf(buff,"%d",ntohs(end_point.sin_port));
std::string cli = inet_ntoa(end_point.sin_addr);
cli += ":";
cli += buff;
msg[s]='\0';
std::cout<<cli<< ":"<< msg <<std::endl;
//std::string echo_string = msg;
std::string echo = "unknow";
auto it = dict.find(msg);
if(it != dict.end())
{
echo = dict[msg];
}
//echo_string += " [server echo!]";
sendto(sock,echo.c_str(),echo.size(),0,(struct sockaddr*)&end_point,len);
}
}
}
~udpServer()
{
close(sock);
}
private:
// std::string ip;
int port;
int sock;
std::map<std::string,std::string> dict;
};
-
udpClient
-
udpServer
7. UDP协议常见问题
7.1 udp报文丢失问题
- 因为UDP自身的特点,UDP会相对于TCP存在一些难以解决的问题。一个就是UDP报文缺失问题。在UDP服务器客户端的例子中,如果客户端发送的数据丢失,服务器会一直等待,直到客户端的合法数据过来。如果服务器的响应在中间被路由丢弃,则客户端会一直阻塞,直到服务器数据过来。
防止这样的永久阻塞的一般方法是给客户的recvfrom调用设置一个超时,大概有这么两种方法
:
- 使用信号SIGALRM为recvfrom设置超时。首先我们为SIGALARM建立一个信号处理函数,并在每次调用前通过alarm设置一个5秒的超时。如果recvfrom被我们的信号处理函数中断了,那就超时重发信息;若正常读到数据了,就关闭报警时钟并继续进行下去。
- 使用select为recvfrom设置超时,设置select函数的第五个参数即可。
7.2 udp报文乱序问题
- 乱序就是发送数据的顺序和接收数据的顺序不一致,例如发送数据的顺序为A、B、C,但是接收到的数据顺序却为:A、C、B。产生这个问题的原因在于,每个
数据报
走的路由并不一样,有的路由顺畅,有的却拥塞,这导致每个数据报到达目的地的顺序就不一样了。UDP协议并不保证数据报的按序接收。
解决这个问题的方法就是发送端在发送数据时加入数据报序号,这样接收端接收到报文后可以先检查数据报的序号,并将它们按序排队,形成有序的数据报。
7.3 udp流量控制问题
- TCP有
滑动窗口进行流量控制和拥塞控制
,UDP因为其特点无法做到。UDP接收数据时直接将数据放进缓冲区内,如果用户没有及时将缓冲区的内容复制出来放好的话,后面的到来的数据会接着往缓冲区放,当缓冲区满时,后来的到的数据就会覆盖先来的数据而造成数据丢失(因为内核使用的UDP缓冲区是环形缓冲区)。因此,一旦发送方在某个时间点爆发性发送消息,接收方将因为来不及接收而发生信息丢失。
解决方法一般采用增大UDP缓冲区
,使接收方的接收能力大于发送方的发送能力
。
转载:https://blog.csdn.net/weixin_45313447/article/details/117261318