现象描述

首先,我们来看这个很简单的代码.
这是一个python的udp服务端和客户端的小demo, 功能是客户端向服务端发送hello, 服务端收到后显示客户端的IP地址信息, 以及客户端发来的信息. 之后服务端会回应给客户端一个hello.
而服务端的代码很简单:

#coding: utf-8

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
bind = ('127.0.0.1', 19191)
s.bind(bind)
while True:
    data, addr = s.recvfrom(1453)
    print(addr)
    print(data)
    s.sendto('hello', addr)

至于客户端的代码, 也是同样的简单:

#coding: utf-8
import socket
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.sendto("hello", ("127.0.0.1", 19191))

这两个代码看上去都没有任何问题.
但是, 假若你真的在Windows上执行了这两个python脚本, 那么服务端有相当大的概率会发生socket.error: [Errno 10054] 远程主机强迫关闭了一个现有的连接的这样一个错误, 然后服务端脚本就崩溃了……
可是如果将这两个脚本放在Linux/Unix上面跑, 你会发现这个脚本完全没有任何问题……
So what happend?


原因调查

如果我们仔细看traceback, 我们可以发现问题是出在data, addr = s.recvfrom(1453)这一行.
而10054的错误代码, 上面我们也已经看到了, 错误信息是远程主机强迫关闭了一个现有的连接.
那么我们现在已经可以知道原因了, 那就是客户端再发送完udp数据包之后, 程序已经退出了. 可是, 这时候服务器还在试图回应一个内容为hello的udp报文, 此时客户端却已经不再可达, 最终造成socket破裂, 产生这个错误.
这样似乎也很容易理解的对吧emmmmmmm……
……
可是!
这是udp呀……udp不是面向无连接的么? 它不像tcp那样还要管理一个连接会话, 在udp数据包发送之后, 至于是否可以到达, 按照udp的理论操作系统是不会去管的.
既然是这个样子, 那上面那种解释根本就是无稽之谈了, 因为udp的通讯根本就没有建立在连接的基础上, 是在凭空发包. 因此操作系统更没有为udp设计连接这种东西.
那老娘请问为什么还会凭空出现一个“远程主机强迫关闭了一个现有的连接”?
这可真是一个非常有意思的事情呢.


Blame Microsoft

查阅各种资料之后, 我发现我一直以来对udp的理解都是没有错的……(这个错误刚开始把我炸的怀疑自己了x
原因其实是这样的, 这理应是Windows的一个bug. 当我们调用python中socket的sendto函数时, 会正常向外发送一个数据包, 这并没有什么问题. 可问题在于, 如果发送的数据包链路层上不可达或出现其他原因的不可达时, 链路层很可能会返回一个ICMP的unreachable报文, 或者操作系统自身产生一个错误代码.
但是问题也就在这了, Windows在处理这些东西的时候, 并没有区分这是udp还是tcp, 一股脑的让recv函数抛出异常, 进而引发了我们刚刚看到的问题.
这个问题个人感觉对个人用户影响并不是非常大, 但对于服务端程序来说却是致命的.
而在Linux/Unix上是不存在这个问题的……


解决办法

既然是Windows的一个bug, 那个人感觉, 想从代码层面上解决估计是非常困难了.
而最好的办法就是用try-except直接忽略掉这个错误, 经过测试, 这样似乎也不会带来什么其他的影响.
修改后的服务端代码如下:

#coding: utf-8

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
bind = ('127.0.0.1', 19191)
s.bind(bind)
while True:
    try:
        data, addr = s.recvfrom(1453)
    except:
        continue
    print(addr)
    print(data)
    s.sendto('hello', addr)

至此, 再次运行上面的脚本, 我们可以发现问题已经成功被解决了呢.