From: "Robert C. Martin" Date: 2002-04-01T02:08:34+09:00 Subject: Multithreaded socket code. Request for Comments I'd appreciate any comments you might have regarding the following multithreaded socket code. I am particularly interested in whether the use of the mutex to protect the array is appropriate, and in whether killing the serviceThread is the right approach in the close method. The first batch of code is the SocketService module. The second batch contains the unit tests. require 'socket' require 'thread' class SocketService def initialize() @threads = [] @lock = Mutex.new end def serve(port, &action) @action = action @ropeSocket = TCPserver.open(port) @serviceThread = Thread.start {serviceTask} end def pendingSessions @threads.size end def serviceTask while true Thread.start(@ropeSocket.accept) {|s| serverTask(s)} end end def serverTask(s) @lock.synchronize {@threads << Thread.current} @action.call(s) s.close @lock.synchronize {@threads.delete(Thread.current)} end def close @serviceThread.kill @ropeSocket.close waitForServers end def waitForServers threads=nil @lock.synchronize {threads = @threads.dup} threads.each {|t| t.join} end end ------------------------------------------ require 'Lapidary/TestCase' require 'SocketService' class TestSocketService< Lapidary::TestCase def setup @ss = SocketService.new() @connections = 0 end def testOneConnection puts "testOneConnection" port = 999 @ss.serve(port) {@connections += 1} connect(port) @ss.close() assertEqual(1, @connections) end def testManyConnections puts "testManyConnections" port = 999 @ss.serve(port) {@connections += 1} 10.times {connect(port)} @ss.close() assertEqual(10, @connections) assertEqual(0, @ss.pendingSessions) end def testSocketSend puts "testSocketSend" port = 999 @ss.serve(port) do |serverSocket| serverSocket.write("hi") end clientSocket = TCPsocket.open("localhost", port) answer = clientSocket.gets clientSocket.close assertEqual("hi", answer) @ss.close() end # We should not be able to keep the service alive by hitting # it with connections after we close it? def testCantKeepAliveByConnectingAfterClose puts "testCloseRaceCondition" port = 999 #set up a service that waits for a message and then dies. @ss.serve(port) do |serverSocket| message = serverSocket.gets end #s1 is a connection to that service. s1 = TCPSocket.open("localhost", port) sleep(.1) #now start closing the server in a separate thread. It cannot #finish closing until s1 completes. Thread.start {@ss.close} sleep(.1) #try to connect to the dying server. s2=nil Thread.start {s2 = TCPSocket.open("localhost", port)} sleep(.1) assertEqual(nil, s2, "shouldn't have connected") assertNotEqual(nil, s1, "Should have connected") #Complete the s1 session. s1.write("testCloseRaceCondition"); s1.close #collect the pending s2 connection testThread = Thread.current @ss.serve(port) {testThread.wakeup} Thread.stop assertNotEqual(nil, s2) s2.close @ss.close end def testSessionCount puts "testSessionCount" port=999 @ss.serve(port) do |serverSocket| message = serverSocket.gets end s1 = nil; Thread.start {s1 = TCPSocket.open("localhost", port)} sleep(.2) assertEqual(1, @ss.pendingSessions); s1.write("testSessionCount"); s1.close sleep(.2) assertEqual(0, @ss.pendingSessions) @ss.close end def connect(port) s = TCPsocket.open("localhost", port) sleep(.1) s.close end end require 'Lapidary/UI/Console/TestRunner' puts "Starting Test" Lapidary::UI::Console::TestRunner.run(TestSocketService) Robert C. Martin | "Uncle Bob" Object Mentor Inc.| unclebob @ objectmentor . com PO Box 5757 | Tel: (800) 338-6716 565 Lakeview Pkwy | Fax: (847) 573-1658 | www.objectmentor.com Suite 135 | | www.XProgramming.com Vernon Hills, IL, | Training and Mentoring | www.junit.org 60061 | OO, XP, Java, C++, Python | You and I can enjoy the experience of not always seeing eye-to-eye, but we can also respect each other, even when you might find some idea of mine totally ludicrous. -- Richard Riehle