1. 程式人生 > >UDP埠複用問題

UDP埠複用問題

    一直覺得UDP協議很簡單,但是今天問題讓我感覺到網路的基礎真是博大精深。

    廢話少說,來看問題吧。由於協議的需要,我得實現一個UDP的客戶端和伺服器端,並且從同一個埠讀寫資料。

    最初不以為然,無非就是用兩個socket,一個監聽並從這個埠讀取資料(伺服器端採用了twisted),另一個向這個埠寫入資料,用python實現只要10行左右的程式碼。

def startServer(queue, port):
    reactor.listenUDP(port, DhtResponseHandler(queue))
    reactor.run()
def sendUdpMsg(self, addr, msg):
    socketHandler = socket.socket(socket.AF_INET,  socket.SOCK_DGRAM)
    socketHandler.bind(("", self.port))
    socketHandler.sendto(msg, addr)
    socketHandler.close()
     由於要向同一個埠寫資料,於是client必須有bind,但是執行後發現server先bind了這個埠,client執行時會報錯
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
http://msdn.microsoft.com/en-us/library/aa924071.aspx
或者unix下的socket)

     這個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()
     感觸良多,底層的知識比較重要,浮沙築高臺果然危險。