UDP埠複用問題
一直覺得UDP協議很簡單,但是今天問題讓我感覺到網路的基礎真是博大精深。
廢話少說,來看問題吧。由於協議的需要,我得實現一個UDP的客戶端和伺服器端,並且從同一個埠讀寫資料。
最初不以為然,無非就是用兩個socket,一個監聽並從這個埠讀取資料(伺服器端採用了twisted),另一個向這個埠寫入資料,用python實現只要10行左右的程式碼。
def startServer(queue, port):
reactor.listenUDP(port, DhtResponseHandler(queue))
reactor.run()
由於要向同一個埠寫資料,於是client必須有bind,但是執行後發現server先bind了這個埠,client執行時會報錯def sendUdpMsg(self, addr, msg): socketHandler = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) socketHandler.bind(("", self.port)) socketHandler.sendto(msg, addr) socketHandler.close()
error: [Errno 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted
一般這種錯誤時因為多個socket不能同時bind同一個地址由於基礎不夠紮實,我開始瘋狂的搜尋,發現有人說埠複用的問題,所謂的埠複用,是指一個套接字釋放掉一個埠後有一個wait_time,另一個套接字如果接著bind就會報錯。雖然我的問題不完全一樣,但是我欣喜若狂的使用了。即在client bind前加上如下一句
socketHandler.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
原因如下:一旦應用程式認為某個連線關閉了,作業系統的網路棧實際上會在一個等待狀態(CLOSE_WAIT, TIME_WAIT)中將該連線的記錄最多儲存4分鐘。也就是試圖宣告某個幾分鐘前在用的埠時,可能只是試圖宣告一個仍在使用的埠。
但是仍然報錯:
error: [Errno 10013] An attempt was made to access a socket in a way forbidden by its access permissions
(順便一提,還有另一個引數叫SO_REUSEPORT,即複用埠,另外有一個叫SO_EXCLUSIVEADDRUSE,即不準複用該埠,其他socket的引數還有很多,可以參考winsock這個10013錯誤讓我百思不得其解,搜尋一下,主要有兩種解釋,有人說是需要提升應用程式的許可權為管理員,我用的是eclipse+pydev,提升完eclipse許可權沒用,實際上還要修改python.exe的許可權,方法是在這個程式上右鍵,相容性一欄中勾上以系統管理員身份執行;有人說是跟其他程式地址或者埠衝突。但是我測試過發現都不行。
另外,執行的時候發現,twisted的伺服器端一定是要在主執行緒中,否則會報signal一定要在主執行緒才能接受的錯誤,但是twisted的reactor一執行起來就阻塞了。
在twisted文件中翻到,原來還有一種UDP叫做connected UDP,變態吧,所謂connected UDP,就是隻能向一個地址收發資料,看起來貌似可以,但是不符合可以向多個地址接收資料。
最後在一篇文章中翻到說需要兩個埠都設定重用,於是我試著重新寫一個伺服器,與之前的客戶端配合,執行良好,完全無錯
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", port))
data, address = sock.recvfrom(4096)
好吧,看來問題在呼叫twisted了,不知道他是否有這樣的設定,進去將這部分程式碼翻了一下,找不到這樣設定的引數。class Port(abstract.FileHandle):
def __init__(self, port, proto, interface='', maxPacketSize=8192,
reactor=None):
"""
Initialize with a numeric port to listen on.
"""
self.port = port
self.protocol = proto
self.readBufferSize = maxPacketSize
self.interface = interface
self.setLogStr()
self._connectedAddr = None
abstract.FileHandle.__init__(self, reactor)
skt = socket.socket(self.addressFamily, self.socketType)
addrLen = _iocp.maxAddrLen(skt.fileno())
self.addressBuffer = _iocp.AllocateReadBuffer(addrLen)
# WSARecvFrom takes an int
self.addressLengthBuffer = _iocp.AllocateReadBuffer(
struct.calcsize('i'))
def startListening(self):
"""
Create and bind my socket, and begin listening on it.
This is called on unserialization, and must be called after creating a
server to begin listening on the specified port.
"""
self._bindSocket()
self._connectToProtocol()
def createSocket(self):
return self.reactor.createSocket(self.addressFamily, self.socketType)
def _bindSocket(self):
try:
skt = self.createSocket()
skt.bind((self.interface, self.port))
except socket.error, le:
raise error.CannotListenError, (self.interface, self.port, le)
# Make sure that if we listened on port 0, we update that to
# reflect what the OS actually assigned us.
self._realPortNumber = skt.getsockname()[1]
log.msg("%s starting on %s" % (
self._getLogPrefix(self.protocol), self._realPortNumber))
self.connected = True
self.socket = skt
self.getFileHandle = self.socket.fileno
難道說twisted就完全不提供這樣的功能?最終在multicast中翻到這樣一段,也就是,多播的情況是支援地址複用的,動手測起來。
class MulticastPort(MulticastMixin, Port):
"""
UDP Port that supports multicasting.
"""
implements(interfaces.IMulticastTransport)
def __init__(self, port, proto, interface='', maxPacketSize=8192,
reactor=None, listenMultiple=False):
Port.__init__(self, port, proto, interface, maxPacketSize, reactor)
self.listenMultiple = listenMultiple
def createSocket(self):
skt = Port.createSocket(self)
if self.listenMultiple:
skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if hasattr(socket, "SO_REUSEPORT"):
skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
return skt
將server端改成如下程式碼,執行通過!reactor.listenMulticast(port, DhtResponseHandler(queue), listenMultiple=True)
reactor.run()
感觸良多,底層的知識比較重要,浮沙築高臺果然危險。