Linux---UDP套接字编程

UDP与TCP网络通信

网络通信在传输层进行的,他实现了两台主机之间通过网络从而达到了通信. 在传输层的典型协议有两个,UDP与TCP, 他们各自拥有各自的特点

  1. UDP
    UDP称为用户数据报协议, 他的特点是 : 无连接, 不可靠, 面向数据报. 意为用户在利用UDP协议进行通信的时候, 不关心对端用户是否在线, 直接发送数据, 同时对数据的安全性能要求没有TCP协议那样的高, 但他要求实时性高, 传输速度快, 因此他可以用于视屏 ,音频的传输.
  2. TCP
    TCP称为传输控制协议, 他的特点是 : 面向链接, 可靠传输, 提供字节流服务 .意为用户在通过TCP协议进行数据传输的时候, 首先有进行链接, 确定对端用户在线,才会进行通信, 并且他对传输的安全性要求高,因此牺牲了部分性能, 传输速度相比于UDP协议较慢.

网络字节序

众所周知, 我们每一台主机的CPU都有自己的主机字节序, 它体现了CPU对数据在内存中的存储顺序, 分为大端字节序(低地址存高位)小端字节序(低地址存低位).

CPU架构: x86_64 (小端字节序序) MIPS (大端字节序)

在同信中, 倘若通信的两方使用的主机的字节序不同, 在进行同行的过程中, 主机字节序为大端的主机,将数据发送给主机字节序为小端的主机后, 由于网络通信中的数据时一个字节一个字节的发送的, 小端主机在接受数据的时候,就会造成数据存储顺序混乱, 这样就产生了数据的二义性 . 因此我们规定了**网络字节序(大端字节序)-----在网络通信中,通信双方都要把主机字节序转换为网络字节序. **

注意 : 由于网络通信中数据是一个字节一个字节的进行发送,因此, 大于一个字节的数据就需要进行转换

在Linux下, 有提供主机字节序转换类网络字节序的接口

//用于32位于16位的数据从主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
//用于32位于16位的数据从网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

在通信中分类了客户端与服务端 , 永远是客户端先向服务端发起请求, 因此服务端需要告诉客户端总结的IP地址域端口信息 . 客户端是主动的一方, 服务端是被动的一方

socket套接字

网络通信 , 就是通过socket这个结构体来实现的, 应用程序可以通过它接受和发送数据, 可以对其进行像文件一样的打开.读写和关闭等操作

  • . 创建套接字
 int socket(int domain, int type, int protocol);

套接字的创建就是利用上述的接口进行创建, 其中有三个参数

  1. domain : 表示创建的套接字的IP地址域:地址与包含IPV4或者IPV6,一般我们用IPV4,其对用的宏伟AF_INIT;
  2. type : 表示创建套接字的的类型, 包含流式套接字(SOCK_STREAM)与数据报式套接字(SOCK_DGRAM)
  3. protocol : 协议—IPPROTO_UDP(17)与IPEROTO_TCP(6)
    其中 0 是使用套接字类型的默认值
  4. 返回值 : socket接口的返回值为一个文件描述符 , 他是创建出来的套接字的操作句柄
  • . 为套接字绑定地址信息
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

bind接口一共包含了三个参数

  1. sockfd : 文件描述符, 操作套接字的句柄
  2. addr : 这个参数就是用来绑定地址信息,
    由于地址与的类型包括IPV4 , IPV6等各种各样的协议类型 ,但是我们提供的接口只有一种,不可能为了各种类型去提供各种接口.IPV4类型的的结构体为struct sockadde_in, IPV6类型的结构体为struct sockaddr_un,他们的结构类型如下图所示
    在这里插入图片描述
    而struct sockaddr结构体得类型为16位地址类型,以及14字节的地址数据
    在这里插入图片描述
    当我们要在IPV4的地址域下进行通信时,只需要定义一个IPV4的结构体,然后将地址类型端口, IP地址初始化到struct sockaddr_in结构体中, 再将这个结构体的类型强制转换成struct sockaddr的类型即可传入接口.
//定义地址域结构体,并且初始化
struct sockaddr_in addr;
addr.sin_family=AF_INIT;
addr.sin_port=htons(port);
addr.sin_addr.s_addr=inet_addr(ip.c_str());
  1. addrlen : 表示地址域的大小, 通过**sizeof()**函数求得
  2. 返回值 : 成功时返回0, 发生错误时, 返回-1.

  • . . 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
             const struct sockaddr *dest_addr, socklen_t addrlen);

sebdto接口包含了六个参数

  1. sockfd : 文件描述符, 套接字的操作句柄
  2. buf : 需要发送的数据或文件的存储首地址.
  3. len : 需要发送的数据或文件的大小.
  4. flags : 标识位, 通常置位0 , 标识阻塞发送, 没有数据就一直阻塞,等待客户端挥着服务端发送数据.
  5. dest_addr与addrlen : 表示要发送到的对端的地址信息和struct sockaddr中的地址域大小.
  6. 返回值 : 成功时, 返回实际发送的字符数, 发生错误时,返回-1 .
  • . . 接受数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
              struct sockaddr *src_addr, socklen_t *addrlen);
  1. sockfd : 文件描述符, 套接字的操作句柄
  2. buf : 接受数据或文件的存储首地址.
  3. len : 需要接受的数据或文件的大小.
  4. flags : 标识位, 通常置位0 , 标识阻塞接受, 没有数据就一直阻塞,等待客户端挥着服务端发送数据.
  5. dest_addr与addrlen : 表示要发送到的对端的地址信息和struct sockaddr中的地址域大小.
  6. 返回值 : 成功时, 返回实际接受的字符数, 发生错误时,返回-1 .
    recvfrom接口中包含了6个参数
  • . 关闭套接字
    由于操作套接字的句柄是一个文件描述符, 因此关闭套接字只需要使用**close()**接口即可

基于UDP协议的网络通信编程

为了实现基于UDP的网络通信编程, 我们需要封装一个UdpSocket类,还需要实现一个客户端与一个服务端.

  • .UdpSocket
class UdpSocket{
public:
	//构造函数
	UdpSocket()
		:_sockfd(-1)
	{}
	//析构函数
	~UdpSocket(){
		Close();
	}
	//套接字的创建
	bool SocketInit(){
		_sockfd=socket(AF_INET,SOCK_DGRAM.IPROTO_UDP);
		if(_sockfd<0){
			cout<<"创建套接字失败!!"<<endl;
			return false;
		}
		return true;
	}
	//绑定地址
	bool Bind(string& ip,uint16_t port){
		struct sockaddr_in addr;
		addr.sin_family=AF_INET;
		//将主机端口转换为网络字节序的端口
		addr.sin_port=htons(port);
		//将字符串类型的IP地址转换为网络字节序的IP地址
		addr.sin_addr.s_addr=inet_addr(ip.c_str());
		socklen_t len=sizeof(addr);
		int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
		if(ret<0){
			cout<<"绑定地址失败!!"<<endl;
			return false;
		}
		return ture;
	}
	//发送数据
	bool Send(string& buf,string& ip,uint16_t port){
		struct sockaddr_in addr;
		addr.sin_family=AF_INET;
		addr.sin_port=htons(port);
		addr.sin_addr.s_addr=inet_addr(ip.c_str());
		socklen_t len=sizeof(addr);
		int ret=sendto(_sockfd,&buf[0],buf.size(),0,
					(struct sockaddr*)&addr ,len);
		if(ret<0){
			cout<<"发送数据失败"<<endl;
			return false;
		}
		return true;
	}
	//接受数据
	bool Recv(string& buf,string& ip.uint16_t port){
		struct sockaddr_in addr;
		addr.sin_family=AF_INET;
		addr.sin_port=htons(port);
		addr.sin_addr.s_addr=inet_addr(ip.c_str());
		socklen_t len=sizeof(addr);
		char tem[4096]={0};
		int ret=recvform(_sockfd,tem,4096,0,
						(struct sockaddr*)&addr,&len);
		if(ret<0){
			cout<<"接受数据失败"<<endl;
			return false;
		}
		buf.assign(tem,ret);
		ip=inet_ntoa(addr.sin_addr);
		port=ntohs(addr.sin_port);	
		return true;
	}
	//关闭套接字
	bool Close(){
		if(_socket<0){
			close(_sockfd);
			_sockfd=-1;
		}
		return trun;
	}
private:
	//私有成员为一个文件描述符
	int _sockfd;
};

注意 :
接受数据的recnfrom()函数中的最后一个参数是一个指针类型的参数, 因此在调用的时候需要对len取地址
在Recv函数中,接受到了数据之后,不经要将数据拷贝出去, 由于服务端是思安接受数据才对客户端进行发送,因此需要记录客户端的IP地址与端口, 但是在函数的内部IP地址与端口是struct sockaddr_in类型的结构体成员, 再传传出去的时候需要将他们分别转换为字符串类型与主机字节序

//将一个in_addr中的IP地址类型, 转换为字符串的类型
char *inet_ntoa(struct in_addr in);
  • UDP服务端
    基于udp的网络通信的服务端, 永远都是先接受数据的
#defind CHECK_RET(Q) if((q)==false){retrun -1;}
//给main函数设定参数是为了在仍服务端运行时让用户自己传入IP地址
int main(int argc,char* argv[]){
	if(argc!=3){
		cout<<"./udp_cli 192.168.14.128 8000"<<endl;
		return -1
	}
	//为套接字提供绑定的ip与port
	string ip=argv[1];
	uint16_t port=argv[2];
	//实例化一个Udpsocket类
	UdpSocket sock;
	CHECK_RET(sock.SocketInit());
	CHECK_RET(sock.Bind(ip,port));
	while(1){
		string buf;
		//用来接收客户端的ip与port
		string recv_port;
		uint16_t recv_port;
		bool ret=sock.Recv(buf,recv_ip,recv_port);
		if(ret==false){
			sock.Close();
			return -1;
		}
		cout<<"客户端说: "<<buf<<endl;
		buf.clear();
		cin>>buf;
		ret=sock.Send(buf,recv_ip,recv_port);
		if(ret==false){
			sock.Close();
			return -1;
		}
	}
	sock.Close();
	return 0;
}

注意
由于服务端需要先启动等待客户端来发送数据, 因此服务端需要自己手动绑定地址, 所以在接受数据的时候需要定义一个局部的 ip 与 port 来接收客户端的ip与port

  • UDP客户端
#defind CHECK_RET(Q) if((q)==false){retrun -1;}
//给main函数设定参数是为了在仍服务端运行时让用户自己传入IP地址
int main(int argc,char* argv[]){
	if(argc!=3){
		cout<<"./udp_cli 192.168.14.128 8000"<<endl;
		return -1
	}
	//为套接字提供绑定的ip与port
	string ip=argv[1];
	uint16_t port=argv[2];
	//实例化一个Udpsocket类
	UdpSocket sock;
	CHECK_RET(sock.SocketInit());
	while(1){
		string buf;
		cin>>buf;
		bool ret=Send(buf,ip,port);
		if(ret==false){
			sock.Close();
			return -1;
		}
		buf.clear();
		ret=sock.Recv(buf,ip,port);
		if(ret==false){
			sock.Close();
			return -1;
		}
		cout<<"服务端说: "<<buf<<endl;
	}
	sock.Close();
	return 0;
}

注意:
客户端可以不手动绑定地址, 在发送数据的时候, 操作系统会自动为客户端选取合适的端口进行绑定

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值