From: Tanaka Akira Date: 2009-01-10T20:11:08+09:00 Subject: [ruby-dev:37750] socket.rb Socket を使っていると、生成するところが TCPSocket 等に比べて 不便なので、以下のようなメソッドをつけてもいいでしょうか。 Socket.tcp(host, port, local_host=nil, local_port=nil) {|socket| ... } Socket.tcp_server_loop(host=nil, port) {|socket, client_addrinfo| ... } Socket.unix(path) {|socket| ... } Socket.unix_server_loop(path) {|socket, client_addrinfo| ... } Socket.tcp と Socket.unix は TCPSocket.open と UNIXSocket.open に対応します。 Socket.tcp_server_loop と Socket.unix_server_loop は TCPServer.open, UNIXServer.open に加えて、accept するループ まで入っています。 accept まで行うのは、TCPServer.open 内で、getaddrinfo が返し たすべてのアドレスを扱うためです。サーバソケットを呼出元に返 す TCPServer.open の形だと、IPv4 と IPv6 を分けて扱うことが できません。別々にソケットを作って分けて扱うというのが RFC 4038 のお薦めなようです。なお、将来的には、引数を拡張し て port を複数指定できるとかしてもいいかもしれません。 あと、Socket.unix_server_loop は既存のソケットファイルを (owner の検査をした上で) 消すようにしてあります。one liner でテストするときにはそっちのほうが便利ですし、まじめにサーバ を書くならどうせ他のサーバが生きているかどうかは別の手段で確 認しなければならず、他にサーバがいない状況で呼び出すとすれば 消しちゃって問題ないと思うので。 実装は Ruby で書いてありますので、socket.rb の新設になります。 % svn diff --diff-cmd diff -x '-u -p' Index: ext/socket/lib/socket.rb =================================================================== --- ext/socket/lib/socket.rb (revision 0) +++ ext/socket/lib/socket.rb (revision 0) @@ -0,0 +1,262 @@ +require 'socket.so' + +class AddrInfo + # iterates over the list of AddrInfo objects obtained by AddrInfo.getaddrinfo. + # + # Example: + # + # AddrInfo.foreach(nil, 80) {|x| p x } + # #=> # + # # # + # # # + # # # + # + def self.foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, &block) + AddrInfo.getaddrinfo(nodename, service, family, socktype, protocol, flags).each(&block) + end +end + +class Socket + # creates a new socket object connected to host:port using TCP. + # + # If local_host:local_port is given, + # the socket is bound to it. + # + # If a block is given, the block is called with the socket. + # The value of the block is returned. + # The socket is closed when this method returns. + # + # If no block is given, the socket is returned. + # + # Example: + # + # Socket.tcp("www.ruby-lang.org", 80) {|sock| + # sock.print "GET / HTTP/1.0\r\n\r\n" + # sock.close_write + # print sock.read + # } + # + def self.tcp(host, port, local_host=nil, local_port=nil) # :yield: socket + last_error = nil + ret = nil + + local_addr_list = nil + if local_host != nil || local_port != nil + local_addr_list = AddrInfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil) + end + + AddrInfo.foreach(host, port, nil, :STREAM).each {|ai| + begin + sock = self.new(ai.pfamily, ai.socktype, ai.protocol) + rescue SystemCallError + last_error = $! + next + end + if local_addr_list + if local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily } + begin + sock.bind(local_addr) + rescue SystemCallError + last_error = $! + next + end + else + next + end + end + begin + sock.connect(ai) + rescue SystemCallError + last_error = $! + sock.close + next + end + ret = sock + break + } + if !ret + if last_error + raise last_error + else + raise SocketError, "no appropriate local address" + end + end + if block_given? + begin + yield ret + ensure + ret.close if !ret.closed? + end + else + ret + end + end + + # creates a TCP server on _port_ and calls the block for each connection accepted. + # The block is called with a socket and a client_address as an AddrInfo object. + # + # If _host_ is specified, it is used with _port_ to determine the server addresses. + # + # The socket is *not* closed when the block returns. + # So application should close it explicitly. + # + # This method calls the block sequentially. + # It means that the next connection is not accepted until the block returns. + # So concurrent mechanism, thread for example, should be used to service multiple clients at a time. + # + # Note that AddrInfo.getaddrinfo is used to determine the server socket addresses. + # When AddrInfo.getaddrinfo returns two or more addresses, + # IPv4 and IPv6 address for example, + # all of them are used. + # Socket.tcp_server_loop succeeds if one socket can be used at least. + # + # Example: + # + # # Sequential echo server. + # # It services only one client at a time. + # Socket.tcp_server_loop(16807) {|sock, client_addrinfo| + # begin + # IO.copy_stream(sock, sock) + # ensure + # sock.close + # end + # } + # + # # Threaded echo server + # # It services multiple clients at a time. + # Socket.tcp_server_loop(16807) {|sock, client_addrinfo| + # Thread.new { + # begin + # IO.copy_stream(sock, sock) + # ensure + # sock.close + # end + # } + # } + # + def self.tcp_server_loop(host=nil, port) # :yield: socket, client_addrinfo + last_error = nil + sockets = [] + AddrInfo.foreach(host, port, nil, :STREAM, nil, Socket::AI_PASSIVE) {|ai| + begin + s = self.new(ai.pfamily, ai.socktype, ai.protocol) + rescue SystemCallError + last_error = $! + next + end + sockets << s + s.setsockopt(:SOCKET, :REUSEADDR, 1) + s.setsockopt(:IPV6, :V6ONLY, 1) if ai.ipv6? + begin + s.bind(ai) + rescue SystemCallError + last_error = $! + next + end + begin + s.listen(5) + rescue SystemCallError + last_error = $! + next + end + } + if sockets.empty? + raise last_error + end + loop { + readable, _, _ = IO.select(sockets) + readable.each {|r| + begin + sock, addr = r.accept_nonblock + rescue Errno::EWOULDBLOCK + next + end + yield sock, addr + } + } + ensure + sockets.each {|s| + s.close if !s.closed? + } + end + + # creates a new socket connected to path using UNIX socket socket. + # + # If a block is given, the block is called with the socket. + # The value of the block is returned. + # The socket is closed when this method returns. + # + # If no block is given, the socket is returned. + # + # Example: + # + # # talk to /tmp/sock socket. + # Socket.unix("/tmp/sock") {|sock| + # t = Thread.new { IO.copy_stream(sock, STDOUT) } + # IO.copy_stream(STDIN, sock) + # t.join + # } + # + def self.unix(path) # :yield: socket + addr = AddrInfo.unix(path) + sock = self.new(:UNIX, :STREAM, 0) + begin + sock.connect(addr) + ensure + sock.close if $! + end + if block_given? + begin + yield sock + ensure + sock.close if !sock.closed? + end + else + sock + end + end + + # creates a UNIX socket server on _path_. + # It calls the block for each socket accepted. + # + # If _host_ is specified, it is used with _port_ to determine the server ports. + # + # The socket is *not* closed when the block returns. + # So application should close it. + # + # This method deletes the socket file pointed by _path_ at first if + # the file is a socket file and it is owned by the user of the application. + # + # Example: + # + # # Sequential echo server. + # # It services only one client at a time. + # Socket.unix_server_loop("/tmp/sock") {|sock, client_addrinfo| + # begin + # IO.copy_stream(sock, sock) + # ensure + # sock.close + # end + # } + # + def self.unix_server_loop(path) # :yield: socket, client_addrinfo + begin + st = File.lstat(path) + rescue Errno::ENOENT + end + if st && st.socket? && st.owned? + File.unlink path + end + serv = self.new(:UNIX, :STREAM, 0) + serv.bind(AddrInfo.unix(path)) + serv.listen(5) + loop { + sock, addr = serv.accept + yield sock, addr + } + ensure + serv.close if serv && !serv.closed? + end + +end + -- [田中 哲][たなか あきら][Tanaka Akira]