SO_REUSEPORT和SO_REUSEADDR与socket编程中那些关于内核自动分配的...
前言:
本文分为三个章节,第一个章节主要是翻译总结汇总一位国外的老兄在Stack Overflow上的回答,但实际上Linux发展这么多年,文中的知识点已经过时且不准确了,
在第二章中通过实验,有更加准确的描述。但是,第一章节也不是全然无用,至少在了解SO_REUSEPORT和SO_REUSEADDR的发展上是有帮助的。
在第三章节中,做实验过程中需要验证一些其他的知识点,因此在这里做一个汇总。
wxy:其实就是我研究完才发现文章写的不对,又不想浪费自己的研究成果,哈哈哈哈哈,hianghiang.....
一:读书笔记
关于Socket基本用法的总结
1,无论tcp还是udp,最终的目的都是为了建立连接(互联网嘛),只不过tcp属于长连接(我自己理解的长连接),udp是短连接
所谓tcp是面向连接的是指,得先有个连接,我才能干活
而udp是面向非连接的是指,没有现成的连接,我直接冲出去
2,关于bind,无论是tcp还是udp,其实都可以省略,tcp在listen的时候自动又内核指定本机地址和端口(未验证)
udp的话,作为客户端更不需要bind,sendto的时候都会自动绑定,即将这个socket类型的文件描述符fd绑定到本机一个地址+端口上 之后在这个socket上蹲守(recvfrom)就可以接收到应答(经过实验)
wxy:关于udp,想要接收到应答其实并不一定要bind,因为发出去的时候内核已经有记录了,只要你recv着(相当于告诉内核我在这个socket上守候着),一旦内核在这个socket上有接收到数据,则就会通知上层守候的那个
如果绑定,那么就意味着我可以不用守着,当服务端回应答的时候,内核自然回上报通知我-----使用select实验下
3,关于系统绑定,
如果指定端口号是"any port"(0),表示,表示的含义是“choose a specific port itself in that case“,即由内核指定的一个在他看来,当前情况下最合适的"一个"
如果指定的ip使用的是"any address"(ipv4通配符为0.0.0.0,ipv6通配符为::),表示的意思是"all source IP addresses of all local interfaces",即由内核将这个socket绑定到本地的所有接口ip上。一旦这个socket被connect了,则Depending on the destination address and the content of the routing table, the system will pick an appropriate source address and replace the "any" binding with a binding to the chosen source IP address.
BSD:
5,关于SO_REUSEADDR
1)如果在为socket进行绑定操作之前设置该标志位,那么这个socket就可以成功绑定到一个地址(ip:port)上,除非另一个socket已经被绑定到和这个地址exactly same的地址上。。
进一步来说,对于绑定到wildcard addresses(ipv4为0.0.0.0, ipv6为::)上,表示的含义是本机的所有ip地址,所以如果该地址已经被绑定上一个socket了,那么其他socket想绑定到设备的某个独立的ip地址上也是会fail (with error EADDRINUSE
),除非这个socket先有设置下SO_REUSEADDR标记。
但是,一个socekt绑定到了一个地址无论是0.0.0.0还是某个特定地址,如果另一socket也绑定到这个一模一样的地址即0.0.0.0或这个特定地址,则会失败,即对exactly the same的地址即使设置了SO_REUSEADDR标记也不好用。
即0.0.0.0就是表示所有ip,如果先有个socketA绑定到某个地址即xxx:port_n,然后socketB想要绑定到0.0.0.0:portt_n则会失败,并不是说会选择"剩下"的地址
以上的描述都是针对端口号相同的,对于端口号不相同的那么不属于冲突,根本没SO_REUSEADDR啥事
2)socket都有一个send buf,当调用send()函数后,数据实际上是被放到了这个buffer中,并没有真正被发送出去。
udp的话,数据接着会很快被发送出去
tcp的话,数据可能会有很长的延迟才会被发送出去,所以如果close tcp socket,很可能遗留一些数据在send buffer中,直接扔了吧不符合tcp宣称的"可靠的连接",所以尽管上层代码以为自己结束了,但是协议栈还在尽力的尝试将这些数据继续发送出去,此时socket的状态就是TIME_WAIT。直到数据被全部发送出去或者超时( Linger Time,一般是2min)了,此时socket才真正的发送出去....
3)内核如果对待处于TIME_WAIT状态的socket
如果没有设置SO_REUSEADDR,那么认为该状态的socket仍然被绑定在之前指定的端口上,或者说这个地址仍然被该socket占用着,所以新socket就不能再绑定在这个地址上,除非这个socket真正closed了
如果有设置,那么即使是exactly the same的地址,也是可以绑定成功的。
6,关于SO_REUSEPORT
1)一旦之前绑定到该ip:port上的socket设置了这个参数,那么再绑定这个地址也没问题,如果这次绑定也设置了SO_REUSEPORT,则还可以继续无限绑定下去...
如果第一个绑定该地址的socket没有设置 SO_REUSEPORT,意思是说我占用了,之后的socket崩想跟老子抢
不同于SO_REUESADDR使用的场景,SO_REUSEPORT参数是给前一个绑定的socket用,表示我与这个地址绑定后,之后的socket是否还能复用
SO_REUESADDR参数是给当前socket使用的,表示我想和这个地址上的其他socket分享该地址
另外,这是一个功能更强大的参数,这个参数对 exactly the same source address都是适用的
2)SO_REUSEPORT不等同于SO_REUSEADDR,也就是说如果一个没有设置SO_REUSEPORT的socket被绑定到一个地址上,然后又一个socket设置了SO_REUSEPORT再绑定,那么必然,是失败的
而且不仅如此,即使之前的那个socket已经正在dying并且处于TIME_WAIT,一样是绑定失败的
那么此时,如果就想能绑定上这个地址怎么办,当然是指在TIME_WAIT状态,则办法有二:
方法一:参考上一节中的内容,在本次绑定的时候配置SO_REUSEADDR选项
方法二:那就让之前绑定到这个地址上的端口都设置成SO_REUSEPORT选项
3)关于Connect()
在前面的描述中,地址复用都是在调用bind()函数的时候失败,实际上在地址复用的情况下,调用Connect()函数也会失败,具体来说
首先,再描述下Connect()函数做了什么?
答:在前面说过,一个连接是由五元组(tuple)组成(协议,源地址,源端口号,目的地址,目的端口号),那么既然地址可以重用,那么就是说可以将两个socket绑定到同一个协议族的同源地址和端口号,
然后如果用这些socket去connect相同的目的地址IP:Port,那么他们的tuple是完全相同的。
这是行不通的,至少对于TCP连接来说这是不行的,对于UDP里来说因为步是真正建立了连接所以勉强可以。
那么如果就真的存在这种连接会怎样呢?答:如果有数据过来,那么系统无法识别这个数据属于哪一个连接,所以要求这种古时候至少目的地址或端口号必须不同才行。这样系统才能正确识别数据到底属于那个连接的。
那么总结来说,为了避免以上的错误,一旦bind的时候是协议,源地址,源port一定要相同,那么在connect的时候如果目的地址也重复了,则后面那个就会报错,报EADDRINUSE,表示你不能再访问这个地址啦,因为相应的五元组已经属于某个存在的连接了....
7,关于Multicast Addresses
所谓多播,就是一对多的连接,如果对于多播地址设置了SO_REUSEADDR选项,那么就可以将多个socket绑定到完全相同的多播地址中,换言之SO_REUSEADDR的作用等同于单播地址的SO_REUSEPORT的作用。实际上,在绑定多播地址的时候,写代码的时候可以将二者看作是一回事,也就是说我们可以用SO_REUSEADDR去实现SO_REUSEPORT的作用
wxy:这里有个问题需要之后验证,即他们的用法是不是还是单播时候的用法,即分别用在当前的socket和之前的socket,然后只不过都达到了可以绑定exactly the same地址的效果?
还是说连用法都一样?
Linux中这两个参数的用法说明
一:Linux版本<3.9
此时的Linux系统只有SO_REUSEADDR选项存在,他的用法和BSD基本上是相同的,除了以下两个重要的区别
.
区别1:一旦某个端口号被一个listen tcp socket(服务器)绑定上了,那么这个端口就不能再被其他socket绑定了,即使设置了SO_REUSEADDR也不能绑定到上。
也就是说,在BSD中,如果第一个socket绑定到了wildcard addresses上,那么只要后面的socket设置了SO_REUSEADDR就可以绑定到一个特定的地址上了
但是在LInux<3.9中,只要前面的socket绑定的是wildcard addresses,就表示所有地址都被我占用了,那么就算这时候是单独的一个特定地址也不行,相同的端口号也不行了
这一点上LInux<3.9要比BSD更严格
wxy:在这里,listening (server) TCP socket该如何理解呢?
有一种翻译是指那种绑定到了wildcard addresses上的,并且启动了Listen了,这不就是server的情况么...
如果上面成立的话,那么是不是就可以理解而非监听(客户)TCP socket则无此限制????
区别2: 对于client sockets,这个选项的效果很像BSD中的SO_REUSEPORT,甚至是使用方式都是在绑定socket之前设置的。为什么会这样呢?
因为为了能让多个socket绑定到 exactly to the same UDP socket address for various protocols,SO_REUSEADDR就相当于代替SO_REUSEPORT的工作了
Linux >= 3.9
此时的Linux系统已经增加了SO_REUSEPORT选项,该选项的作用完全和BSD相同,也就是说只要大家都在绑定之前设置该参数,那么就可以一直一直绑定到exactly the same地址和端口号
当然,还是有两个区别需要说明下
1,为了放置"端口劫持",这里有一个限制:想要共享地址:port,必须是那些具有相同User ID的进程范围内。这样一个用户就不能把其他用户的的端口偷走了。
另外,This is some special magic to somewhat compensate for the missing SO_EXCLBIND
/SO_EXCLUSIVEADDRUSE
flags.
2,除此之外,Linux系统还有一个特别功能对于SO_REUSEPORT
sockets ,那就是:
对于UDP socket,内核会尽量为这些共享地址的socket平分数据报
对于TCP listenning socket, 内核会尽量为这些共享地址的socket平分进来的连接请求(即通过调用accept()得到的连接请求),或者说
这些listen中的socket都在等待着,基本上大家accept到的连接是平均的。
所以,有一些应用常常创建多个子进程,然后利用SO_REUSEPORT选项复用相同的port,这样依托于内核的这种平分机制实现一个简单的负载均衡。
二:SO_REUSEPORT和SO_REUSEADDR用法实操
我的实验
0,环境
# uname -a
Linux one1-asm-hx 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
一:针对Bind socket:
int on=1; //这个非常重要,因为他表示开关,1表示设置这个类型的参数,0表示关闭?这个参数
//1首先,创建一个socket,并且绑定到指定端口号上
fd= socket(PF_INET, SOCK_STREAM, 0); setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); //我告诉别人可以复用我所绑定的端口号 res=tcpBind(fd,ip,i); //ip地址可以是任何,包括0.0.0.0,即INADDR_ANY
//2然后,直接又创建一个socket并且绑定到相同的端口号上 int fd2= socket(PF_INET, SOCK_STREAM, 0); setsockopt(fd2, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); //我要复用这个端口号,尽管这个端口号可能被别人占用了 setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); res=tcpBind(fd2,ip,i);
结果:
测试1: 同一进程中的两次绑定:
若,均不设置任何参数,顺序执行1和2,则2绑定失败;
若,1和2全部设置SO_REUSEADDR/SO_REUSEPORT,则2可以绑定成功;
若,任意设置一个也没用(2种参数都是),2还是绑定不成功,都是98
测试2:杀死进程,立即重新启动,无需设置,没问题,可以绑定成功
测试3:同时启动2个进程,绑定相同的端口号,
若,不设置参数,绑定失败,错误码98,
若,设置SO_REUSEADDR或SO_REUSEPORT,则第二个进程可以绑定成功
小小结:在绑定阶段,SO_REUSEADDR和SO_REUSEPORT在用法上没有区别,想要达到复用的效果,必须所有socket在绑定之前就设置该参数
二:针对Listen socket:
测试1:
//1)创建,绑定,监听socket,此时查看该端口号,处于"LISTEN "态 fd= socket(PF_INET, SOCK_STREAM, 0); res=tcpBind(fd,ip,i); res=listen(fd,1024);
测试1: 同一进程中的2次绑定+ 监听,
若,1和2全部只设置SO_REUSEADDR,则2监听失败:[Error]Listen on this socket2 error:98;
若,1和2全部只设置SO_REUSEPORT,则1和2均监听成功:[All Success]Listen on this socket 1 and socket2 all success;
若,1全部设置,2只设置 SO_REUSEADDR(保证绑定是可以成功),则2监听失败
2只设置SO_REUSEPORT(保证绑定是可以成功),则2监听可以成功 --这个自然是,同第二个若...
测试2: 无需设置参数,杀死进程,立刻重起,没问题,可以绑定成功
测试3: 同时启动2个进程,绑定相同的端口号
同测试1;
小小结:对于为socket设置监听的话,SO_REUSEADDR和SO_REUSEPORT在用法上就有区别了,设置前者则不能复用端口去监听,设置后者则可复用端口号去监听了
并且要求所有复用的socket都要设置该参数,并不是向那篇文章所说的,只要前一个设置了SO_REUSEPORT,就表示后面那个可以去用了...
三:针对派生出连接的socket:
1)创建,绑定,监听,并且还接收了一个客户端的连接
fd= socket(PF_INET, SOCK_STREAM, 0); //setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on,sizeof(on)); //setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on,sizeof(on)); res=tcpBind(fd,ip,i); res=listen(fd,1024); int accept_fd=accept(fd,(struct sockaddr*)&client_addr,&len);
此时查看端口号的状态可以发现,30000仍然处于Listen状态,但是派生出来一个新的连接,处于ESTABLISHED状态
# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 0.0.0.0:* LISTEN 19260/tcp_server
tcp 0 0 192.168.1.158:30000 192.168.1.181:61003 ESTABLISHED 19260/tcp_server
测试1:断开客户端的连接,此时
[root@localhost ~]# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 0.0.0.0:* LISTEN 19260/tcp_server
tcp 1 0 192.168.1.158:30000 192.168.1.181:61003 CLOSE_WAIT 19260/tcp_server
wxy:在这系列的测试中,断开客户端服务端的端口直接进入CLOSE_XX状态,所以端口号可以立即使用,基本并不会影响server端,顶多是派生出来的那个连接状态会有所改变,所以无需继续测试。
测试2:客户端还在连接着的时候,关闭服务端进程,于是
[root@localhost ~]# date
Fri Dec 13 20:43:23 CST 2019
[root@localhost ~]# netstat -auntp|grep 30000
tcp 0 0 192.168.1.158:30000 192.168.1.181:61129 TIME_WAIT -
此时重起服务进程,则报错:Bind 192.168.1.158:30000 error:98
经过一段时间后,大约1min,2MSL:
[root@localhost ~]# netstat -auntp|grep 30000 ---表示已经释放端口了
[root@localhost ~]# date
Fri Dec 13 20:44:24 CST 2019
此时再重起服务进程,则成功。
测试3:在测试2的基础所上
若,增加SO_REUSEADDR,则可以瞬间重新建立监听,并接收连接;
1)如果前一次有设置,后一次没有设置,则后一次绑定失败
2)如果前一次没有设置,后一次有设置,则后一次绑定失败
若,增加SO_REUSEPORT选项也可以瞬间重新建立监听,并接收连接,但还有三个和SO_REUSEADDR不同的点
1)还可以同时启动多个进程,随即接收客户端的连接,一旦一个进程结束,连接断开,重新连的话就会和另一个进程连接上。这个是ADDR做不到的
2)如果前一次没有设置或者设置的是SO_REUSEADDR选项,则在2MSL期间,即使第二次代码已经设置了SO_REUSEPORT,同样连接是失败的。
3)如果前一次设置的是SO_REUSEPORT,进入TIME_WAIT后,若第二次没有设置(设置了SO_REUSEADDR也不行),则都会连接失败
小小结:SO_REUSEADDR主要是用在当端口进入TIME_WAIT后,可以复用的场景,要求前后两次都要设置了该参数才行
SO_REUSEPORT则是直接可以复用一个端口,同样要求前后或同时都设置该标记。
总的来说,SO_REUSEPORT是后来新增的选项,功能几乎覆盖了SO_REUSEADDR更强大,貌似只又一点不同,针对不同的userid的进程,复用时有额外的要求,见下面。
源码解析:
SO_REUSEADDR
Indicates that the rules used in validating addresses supplied in a bind(2) call should allow reuse of local addresses.
For AF_INET sockets this means that a socket may bind, except when there is an active listening socket bound to the address.
When the listening socket is bound to INADDR_ANY with a spe‐cific port then it is not possible to bind to this port for any local address.
解析:1)这个参数是用于bind(),即想要让不同的socket复用local address(ip:port),除了2)种约定,也就是说socket处于初始状态和Time_Wait状态都可以绑定成功(和实验一致)
2)如果这个地址已经被某个处于listening态的socket绑定了,那就不能再复用了,或者说绑定在上面的(和实验一致)
3)这里面有一个特殊的地址0.0.0.0:port,他可以认为是和任何一个local address 相同,所以是否能够复用也要遵循1)和2)
另外,wxy认为,man7中其实说的很准确,也就是说在客户端只要设置了这个参数就可以复用了,因为客户端不存在listen,尽管我还没有做实验....
SO_REUSEPORT
The new socket option allows multiple sockets on the same host to bind to the same port, and is intended to improve
the performance of multithreaded network server applications running on top of multicore systems.
解析:用于多线程的
源码(3.10.0-693.el7内核版本,即CentOS 7.4):
int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax)
{
struct sock *sk2;
int reuse = sk->sk_reuse; //本次要绑定的socket的地址可重用标志
int reuseport = sk->sk_reuseport; //本次要绑定的socket的端口可重用标志
kuid_t uid = sock_i_uid((struct sock *)sk);//该连接的用户id
sk_for_each_bound(sk2, &tb->owners) { //遍历绑定在本端口上的所有socket:sk2就做为每一次检查的参照物
//1.第一关的检查:如果不符合,就跳过本轮取得下一个参照物再来;如果符合进入第二关
if (sk != sk2 && !inet_v6_ipv6only(sk2) && //1.1:这次绑定的socket和参照物不是一个; 并且参照物不是ipv6only???
(!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if || //1.3:并且本socket和参照物如果有一个没设置SO_BINDTODEVICE,或者都设置了但相等(即绑定了同一个网络设备)
sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) {
//2.第二.1关的检查:如果不符合转如二.2关检查;如果符合进入第三关
if ((!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) && //2.1:当前socket没有设置addrreuse; 或者参照物没有设置addrreuse; 或者参照物处于LISTEN态
(!reuseport || !sk2->sk_reuseport || (sk2->sk_state != TCP_TIME_WAIT && //2.2:并且也没有设置portreuse;或者参照物没有设置portreuse;或者和参照物属于不同用于id,而参照物的状态又不是TIME_WAIT态
!uid_eq(uid, sock_i_uid(sk2))))) {
//第三关:如果符合则证明冲突了,不再检查其他参照物;否则重新进入二.2关检查
if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || //3.1参照物的socket还没有rcv地址(还没有和某端建立连接);或者当前socket也没有rcv地址;或者都有并且相同
sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
break;//冲突了 //综上,如果1,2,3组条件都满足了,那么冲突无疑了
}
//2.第二.2关的检查:relax为false,且当前连接和表中连接都允许地址重用且表中连接状态不为listen
if (!relax && reuse && sk2->sk_reuse && sk2->sk_state != TCP_LISTEN) { //2.1:relax为false并且当前端口和参照物都设置了addr reuse,并且参照物也没有处于LISTEN状态
//第三关:如果符合就证明冲突了,不再检查其他参照物;否则进行下一轮参照物的检查
if (!sk2->sk_rcv_saddr || !sk->sk_rcv_saddr || //3.1同上
sk2->sk_rcv_saddr == sk->sk_rcv_saddr)
break;//冲突了
}
}
}
return sk2 != NULL; //表示在轮循的过程中,已经绑定到本端口上的所有socket中存在一个满足条件的socket,于是冲突了
}
根据源码,可以总结如下情况不会冲突,即地址可以复用
1,同一个socket,重复绑定
2,绑定的不是用一个interface,即ip
3,都设置了SO_REUSEADDR标志,且早前的socket并没有进入LISTEN态(当然处于TIME_WAIT态也算没有进入LISTEN状态,所以可以重用)
4,都设置了SO_REUSEPORT标志,且早前的socket已经进入TIME_WAIT态或者这些socket属于一个uid的
wxy:从上面的代码也可以看出来,不像早前那篇文章所描绘的,SO_REUSEADDR用在当前要绑定的socket上,而SO_REUSEPORT用在之前的socket上,作用于当前socekt....
小小结:socekt是否阻塞模式和epoll一点关系没有,epoll只管看是否有事件发生,有则调用相应的"处理逻辑"处理。为什么有的时候要求设置非阻塞模式,那是为了"处理逻辑"能顺利执行下去,因为在epoll模型中,一般都是同事检测着多路socket,所以发生wait出来的往往是好几个时间,而"处理逻辑"肯定得是一个一个处理,因此如果因为一个socket阻塞在那里,就影响其他socket了,所以根据各种场景按需设置socket为非阻塞模式。
三:其他知识点
1,关于tcp的四次挥手
A --------tcp---------------B
A说:我要断开连接,所以应用程序调用close()函数,内核得知后有如下行为
首先发送FIN=1,seq=u报文给B,A进入--->FIN-WAIT-1态
B听:内核接收消息后,回应ACK=1,seq=v,ack=u+1,并进入--->ClOSE-WAIT态,同时
A听:收到B的ack后,状态从FIN-WAIT-1--->FIN-WAIT-2
B说: 内核回应A的同时还会通知上层应用层,即发EOF(end of file)消息,于是上层也应该调用close()函数
同时发送FIN=1,ACK=1,seq=w,ack=u+1给A,B变成--->LAST-ACK态;
A听:内核接收到消息后,回应ACK=1,seq=u+1,ack=w+1给B,同时进入--->TIME-WAIT态
经过2MSL时间后,最终进入--->CLOSED态
B听:收到A的ack后,转态从LAST-ACK--->CLOSED
wxy:关于这个经过实验可以发现:
Server端代码: ip="0.0.0.0"; port=0; //1.绑定时 sockaddr_in server_addr,local_addr; server_addr.sin_addr.s_addr = inet_addr(ip.c_str()); server_addr.sin_port = htons(port); int res=bind(fd, (struct sockaddr*)&server_addr, addrlen); getsockname(fd, (struct sockaddr*)&local_addr, &addrlen); printf("After bind to any adress, the fact address which socket use is:[%s:%d]\n",inet_ntoa(local_addr.sin_addr),ntohs(local_addr.sin_port)); //2,接收到客户端的连接后(通过epoll) int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len); //3.调用完接收函数后 getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len); printf(" After recv from client, Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
Client:
sock = socket(AF_INET,SOCK_DGRAM, 0);
getsockname(sock, (struct sockaddr*)&local_addr, &len);
printf("***creat the socket:%d and Local[%s:%d]\n",sock,inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
int res=sendto(sock, Data, sizeof(Data), 0 , (struct sockaddr*)&server_addr, len);
getsockname(sock, (struct sockaddr*)&local_addr, &len);
printf("***after send to server:%d and Local[%s:%d]\n",sock,inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
res = recvfrom(sock,recvBuf,PACKAGE_LEN,0, (struct sockaddr*)&server_recv,&len); printf(" Get echo From[%s:%d] by[:%d] success\n",inet_ntoa(server_recv.sin_addr), ntohs(server_recv.sin_port), ntohs(local_addr.sin_port));
结果:
# ./udp_server2 After bind to any adress, the socket use the fact address is:[0.0.0.0:53366] //第一次客户端通过182网段的接口发送请求 After epoll event happend, Local address[0.0.0.0:53366]. After recv from client [182.168.1.246:48410], Local address[0.0.0.0:53366]. //第二次客户端通过192网断的接口发送请求 After epoll event happend, Local address[0.0.0.0:53366]. After recv from client [192.168.1.246:22119], Local address[0.0.0.0:53366].
#./udp_client 182.168.1.245 53366
Get echo From[182.168.1.245:53366] by[:20937] success //此时的客户端获取的地址确实是相应的动态变化
# ./udp_client 192.168.1.245 53366
Get echo From[192.168.1.245:39964] by[:19950] success
另外,客户端使用的端口号再发送报文的时候就被指定了:
***creat the socket:5 and Local[0.0.0.0:0]
***after send to server:5 and Local[0.0.0.0:56648]
***after recv to server:5 and Local[0.0.0.0:56648]
抓包:
服务端确实会根据访问接口调整发送包的接收及ip,且并不会在所有接口上洪泛....
结论:
服务端绑定的时候是0.0.0.0,表示确实被绑定到本机的所有接口上,但是真正数据报从哪个接口出去,则是由内核根据情况界定,即在被连接后确实根据客户端的请求指定相应的接口及ip去回应,但是此时无论是在接收请求之后还是数据报都sendto出去了,getsockname时,仍然获取的是0.0.0.0
对于客户端:
并不需要绑定,创建socket的时候相当于socket还没有指向哪个具体的地址,一旦报文发送出去,端口号就自动指定了(当然地址应该也指定了,但是直接获取是获取不到的)
再这个socket上阻塞recvfrom,则如果有应答回来,则必然会从这个端口号/fd中进来
wxy:如果发现client再send之后端口还是0,或者sever接收后得到客户端的端口为0,那么不用怀疑,一定是你的代码中有写错了,因为端口0只能是人为指定用来告诉内核去指定一个可用的端口号作为真正使用的....
4,关于udp的connect
Server端:
... //实验1:从客户端接收到报文后之,向client发送应答之前,将这个socket固定给这个客户端
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len); connect(events[i].data.fd,(struct sockaddr*)&client_addr,len); bzero(&local_addr, sizeof(local_addr)); getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len); printf(" After connect client, Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
//实验2:同样还实验了绑定之后直接就connect操作(当然,这么做没什么意义,因为作为服务端怎么能固定客户端的地址和端口号呢)
bind(fd, (struct sockaddr*)&server_addr, addrlen);
connect(fd,(struct sockaddr*)&client_addr,len); //此时可以获取到系统给socket绑定的地址
结果:没意义,客户端除非绑定,否则无法刚好使用server端connect的地址(端口号)
//实验3: 接收报文后返回应答,再用这个socket向其他设备且是不同网段的发送数据
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len);
int send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&client_addr,len);
send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&tmp_addr,len); //"192.168.1.181:50000"
bzero(&local_addr, sizeof(local_addr));
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
结果:初次,数据从182网段接口接收,然后从该接口回送应答后,再向192.168.1.181发送报文,也是正常的。接收初次的第二个报文也都是正常
第二次,即客户端会使用的端口号会变,同样收发正常.
第n次,OK...
//实验4:接收到报文后,connect到发起者,再回送应答,最后向其他设备发送数据
int recv_cnt = recvfrom(events[i].data.fd, recvbuf, BUF_LEN, 0, (struct sockaddr*)&client_addr,&len);
connect(events[i].data.fd,(struct sockaddr*)&client_addr,len);
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
//int send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&client_addr,len);
int send_cnt=send(events[i].data.fd, echoBuf, sizeof(echoBuf),0); //注意,在实验4中,使用send()和sendto实验效果相同
send_cnt=sendto(events[i].data.fd, echoBuf, sizeof(echoBuf), 0, (struct sockaddr*)&tmp_addr,len);
getsockname(events[i].data.fd, (struct sockaddr*)&local_addr, &len);
printf(" After connect and send echo to client, Send to other(192.168.1.181:50000), Local address[%s:%d].\n",inet_ntoa(local_addr.sin_addr), ntohs(local_addr.sin_port));
结果:效果等同关于实验1,并且向另一个设备181也是正常的,无论客户端使用192网段还是182网段发送报文
结果(实验1):
客户端执行: # ./udp_client 182.168.1.245 40122 2 //初次发送,2次 Send 182.168.1.245:40122 by [:27275]成功. Get echo From[182.168.1.245:40122] by[:27275] success Send182.168.1.245:40122 by [:27275]成功. Get echo From[182.168.1.245:40122] by[:27275] success # ./udp_client 192.168.1.245 40122 40122 2 //再次发送,无论是原来的ip还是新的ip,服务端都不再接收 Send 192.168.1.245:40122 by [:16009]成功. //没有接收到服务端的echo 服务端效果: # ./udp_server2 //初次接收客户端的连接时,获取的本地地址同样是0,但一旦connect,初次接收的第二次发送就编程有地址了 After bind to any adress, the socket use the fact address is:[0.0.0.0:40122] After epoll event happend, Local address[0.0.0.0:40122]. After recv from client, Local address[0.0.0.0:40122]. After connect client, Local address[182.168.1.245:40122]. After epoll event happend, Local address[182.168.1.245:40122]. //初次接收时的第二次发送 After recv from client, Local address[182.168.1.245:40122]. After connect client, Local address[182.168.1.245:40122].
//之后就在也无法接收别人的连接
结论:udp,如果接口进行了connect操作,相当于提前为socket指定了方向,所以对于那些绑定到0.0.0.0的socket,无需等到被连接或者主动连接内核才会为其指定具体的ip。
一旦一个socket被绑定到一个指定地址(ip:port), 那么这个socket就在也无法接收来自其他的连接了; 但是他向其他地址发送报文是不受影响的
注:关于inet_ntoa函数
"The return value is a pointer into a statically-allocated buffer. Subsequent calls will overwrite the same buffer, so you should copy the string if you need to save it."
解析:经过转化后的地址是放在一块静态内存中,即所有的转化结果都放置在一块内存中,如果转化后不立刻打印或者使用,则如果接下来还有转化操作,就会覆盖掉之前的转化
-----------------------------------------------------
---------------------------------------------------
-----------------------------------------------------------------------------------------------