winsock服务端获取客户端ip_网络工程师(12):让IP更容易得到

c5fc35e53ebf5736ff6c483ed80116da.png

我们从开始到现在,用了十一篇小文章讨论和解决了网络中的基本通信问题。以后的主要任务,是对网络进行优化:比如让网络在速度、性能、健壮、安全等多方面进行提升,比如可以更容易上网,比如让网管人员工作更轻松,等等。当然,还可以根据需要,给网络增加新的功能。

第一个要优化的,就是IP地址!

IP地址?这还能怎么优化?

IP地址标识了你在网络中是谁,别人找你或者你找别人都会用到它。可是,在电脑上配置IP地址可不是件容易的事,需要你了解IP地址、掩码、网关等知识。可是咱不能因为不懂这些就不能上网呀,所以得有个解决办法。

DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)就是一个能让电脑获得IP地址的协议。

DHCP是一个应用层协议,分为服务端和客户端,基于UDP传输,端口号服务端使用67,客户端使用68。需要IP地址的电脑都可以叫客户端。当电脑开机后,可以自动从服务端获取所需的IP地址、掩码、网关等信息,完全不需要我们参与。

我们来讨论下DHCP都干了啥。

客户端电脑开机后,会自动产生一个叫DHCP Discover的报文,写入自己的MAC地址,然后交给传输层UDP封装,源端口号填写68,目的端口号填写67,向下交给网络层IP。IP封装时,源IP地址填写0.0.0.0(客户电脑还没有IP地址),目的IP地址填写255.255.255.255(客户端没有IP地址,并不知道自己在哪个网段,所以无法使用网段广播地址。当然更不知道DHCP服务器在哪,所以只能封装一个全网广播地址),协议号填写17(表示是UDP给下来的。TCP的还记得不?),向下交给链路层以太网,以太网封装时填写源MAC地址为客户端电脑的MAC,目的MAC填写FFFF-FFFF-FFFF广播地址,Type填写0x0800。封装完成后发送出去。

5447a849628f776af1f62e87e5dc7b8d.png

服务端收到后,记录Discover报文中的MAC地址,产生一个DHCP Offer报文,把准备分配给客户端的IP地址、掩码、网关、租期、客户端MAC等信息写在报文中,交给UDP。UDP封装源端口号67,目的端口号68,交给IP。IP封装源地址为DHCP服务器地址,目的地址为255.255.255.255,协议号17,交给以太网。以太网封装源MAC为服务器MAC地址,目的地址为FFFF-FFFF-FFFF广播地址,Type为0x0800。封装完成后发送出去。

acd7407e5d3c67aa66a5548c6624b483.png

客户端收到Offer报文后,检查里面的MAC如果是自己的,会封装一个Request报文发出去。为什么要发这个Request呢?Offer报文里IP信息都有了,客户端干嘛不直接用,还要Request请求一下呢?

这是因为,网络中可能存在多台DHCP服务器,每台服务器收到Discover后,都会预留一个预分配地址并写进Offer发出去,客户端可能会收到多份Offer。所以,客户端发出Request的目的就是做一个选择,只能要一个DHCP服务器分配的地址,并把选择的DHCP服务器的IP地址写在Request中,其他服务器收到Request后,发现没有选择自己,就会把刚才准备分配的IP地址释放掉。

客户端一般选择收到的第一个Offer。

752253354f28803e9c5e28e48c1f45fd.png

Request的IP封装中,源地址为什么不使用服务器在Offer中分配的地址,还是使用0.0.0.0呢?

主要原因是现在还不知道有没有其他人在使用这个地址。如果网络中的地址全是由同一台DHCP服务器分配的,不会出现地址冲突。但更多情况下,有很多设备的IP地址是手工填写的,而这些手工填写的IP地址,DHCP服务器并不知情。

服务器收到Request报文后,看看里面的MAC地址之前有没有记录过,如果有,则把客户端MAC写进DHCP ACK报文,封装后发出去。ACK报文内容和封装与Offer报文基本相同。如果没有,则把这个Request报文中携带的客户端MAC封装进一个NAK消息中发出,通知这个客户端无法给它分配地址(意思是服务器没有收到这个客户端的Discover,直接收到了它的Request。如果收到Discover肯定有记录)。客户端收到NAK后会重新发出Discover申请地址。

客户端收到ACK报文后,封装一个免费ARP报文,Sender IP和Target IP都填写为服务器分配的IP地址(在服务器发的Offer和ACK报文中都有)发出去,检查此地址是否可用。如果收到ARP Reply回复,则表示此地址已被使用,客户端则封装一个DHCP Decline报文发给服务器,告诉服务器禁用这个地址,然后发出Discover重新开始地址申请。若客户端没有收到ARP Reply,表示此地址可用,开始使用这个地址并开始计算租期。

我们可以发现,客户端在申请地址过程中,需要与服务器进行多次通信,双方每次通信基本靠“吼”(广播),如果客户端数量较多,就要考虑大量报文在传输中别出岔子。把客户端MAC地址写进每个报文中是最好的办法。不然服务器说这个Discover谁发的呀,客户端说这个Offer是给谁分配的地址呀,如果大家都用第一个Offer,地址不就冲突了嘛。

客户端在使用地址期间,随时可以通过DHCP Release报文释放此地址。服务器收到Release报文后,收回该地址重新参与分配。

客户端使用地址的时间到达租期的50%时,会封装DHCP Request报文发出(对,还是Request,只是这个Request封装的源IP是客户端正在使用的IP地址,目的IP是DHCP服务器的IP地址,两人单播联系,小声耳语,别人听不到),服务器收到后,回复DHCP ACK报文。如果客户端收到这个ACK报文,则重新计算租期,否则继续使用这个地址。

当客户端使用地址的时间到达租期的87.5%时,再次封装DHCP Request报文发出(这个Request跟50%那次有点不同,目的地址不是DHCP服务器的IP地址而是255.255.255.255广播地址。因为上次单播时服务器没有理我,没办法只好再次使出“狮吼功”),服务器收到后,回复DHCP ACK报文。如果客户端收到ACK,重新计算租期,否则依然继续使用这个地址。

当客户端使用地址的时间到达租期,则封装DHCP Release释放此地址,重新开始新的地址申请过程(“吼”了也没人理我,算了这个地址我不要了,重新申请去)。

我们举个栗子:假如地址租期为24小时(一般默认是这个时间)。客户端使用地址12小时,小声单播发出DHCP Request报文,如果收到服务器的ACK报文,则重新计时。否则继续使用但不重新计时。使用到21小时,大声吼出Request报文,如果收到ACK,租期重新计算,否则继续使用但不重新计时。使用到24小时,客户端发出Release释放这个地址不要了,重新发Discover申请地址。

我们发现,虽然UDP协议不可靠,但DHCP已有应对办法。关键是,因为客户端没有IP地址,根本无法使用TCP去建立连接。所以,UDP协议在很多场合下非常有用,并没有因为它不可靠而显得不堪。通常,应用程序使用UDP作传输协议时,都已经解决了它不可靠的问题。

小Q:地址申请过程中的Discover、Offer、Request和ACK都被IP和以太网封装成了广播,那么,如果服务器和客户端不在同一个网段怎么办?

欢迎大家留言讨论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 使用以下代码: let socket_addr: std::net::SocketAddr = // ...; let win_socket_addr: windows_sys::Win32::Networking::WinSock::SOCKADDR_IN = socket_addr.into(); ### 回答2: 要将std::net::SocketAddr转化为windows_sys::Win32::Networking::WinSock::SOCKADDR_IN,可以使用以下Rust代码: ```rust use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use winapi::um::winsock2::SOCKADDR_IN; use winapi::shared::ws2def::{AF_INET, AF_INET6}; use winapi::shared::in6addr::in6addr_any; fn socket_addr_to_sockaddr_in(socket_addr: &SocketAddr) -> SOCKADDR_IN { let ip_addr = socket_addr.ip(); let port = socket_addr.port(); match ip_addr { IpAddr::V4(ipv4_addr) => { let mut sockaddr_in: SOCKADDR_IN = unsafe { std::mem::zeroed() }; sockaddr_in.sin_family = AF_INET as u16; sockaddr_in.sin_addr.S_un.S_addr = u32::from(ipv4_addr); sockaddr_in.sin_port = port.to_be(); sockaddr_in } IpAddr::V6(ipv6_addr) => { let mut sockaddr_in6: SOCKADDR_IN = unsafe { std::mem::zeroed() }; sockaddr_in6.sin6_family = AF_INET6 as u16; sockaddr_in6.sin6_port = port.to_be(); sockaddr_in6.sin6_addr = *ipv6_addr.segments(); if *ipv6_addr == Ipv6Addr::from(in6addr_any) { sockaddr_in6.sin6_addr = *in6addr_any; } sockaddr_in6 } } } fn main() { let socket_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); let sockaddr_in = socket_addr_to_sockaddr_in(&socket_addr); // Do something with the converted SOCKADDR_IN println!( "Converted SOCKADDR_IN: {{ family: {}, addr: {}, port: {} }}", sockaddr_in.sin_family, sockaddr_in.sin_addr.S_un.S_addr, sockaddr_in.sin_port ); } ``` 以上代码使用Rust的winapi库来访问Windows Socket API,并将std::net::SocketAddr的IP地址和端口转换为windows_sys::Win32::Networking::WinSock::SOCKADDR_IN结构体。根据输入的std::net::SocketAddr类型,使用IF_INET或IF_INET6来设置SOCKADDR_IN的family字段。然后将IP地址和端口填充到SOCKADDR_IN结构体中。在这个例子中,我们将std::net::SocketAddr转化为127.0.0.1:8080的SOCKADDR_IN。 ### 回答3: 使用Rust编写的代码如下所示: ```rust use std::net::SocketAddr; use windows_sys::Win32::Networking::WinSock::SOCKADDR_IN; fn main() { let socket_addr = SocketAddr::new("127.0.0.1".parse().unwrap(), 8080); let sockaddr_in = SOCKADDR_IN { sin_family: 2, // AF_INET sin_port: socket_addr.port().to_be(), sin_addr: socket_addr.ip().octets().into(), sin_zero: [0; 8], // initialize sin_zero with zeros }; println!("SOCKADDR_IN: {:?}", sockaddr_in); } ``` 以上代码首先导入了`std::net::SocketAddr`和`windows_sys::Win32::Networking::WinSock::SOCKADDR_IN`模块。然后创建了一个`SocketAddr`实例,该实例包含了IP地址和端口号。接下来,我们定义了一个`SOCKADDR_IN`结构体变量,并根据`SocketAddr`的信息进行初始化。最后,打印输出了转换后的`SOCKADDR_IN`实例。 这段代码将`std::net::SocketAddr`转化为了`windows_sys::Win32::Networking::WinSock::SOCKADDR_IN`。在转换过程中,我们将IP地址转化为字节数组,然后将端口号进行了字节序的转换。最后,我们打印输出了转换后的结果,以便验证转换是否成功。 注意:本示例中使用的`sockets` crate中的`windows-sys`模块提供了便于与Windows系统交互的Rust绑定,以简化开发过程。请在使用之前确认已在`Cargo.toml`中添加相应的依赖。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值