Ruby使用Socket处理Http请求

1 引言

经过了一周的ruby学习后,为了让ruby基础得到巩固,我用socket写了一个处理http请求的gem包,我也不知道是否已经有大神完成类似功能的gem包,也不清楚我写的这个服务有什么实际的作用,这个项目也只是作为练手。

首先要吐槽一下的是,这个项目虽然代码量不是很多,但是对一个ruby初学者来说,全程用sublime来写还是蛮累的,建议使用RubyMine。

用sublime写代码总结的坑如下:

  • 使用sublime编写完代码在irb上执行查看结果,若.rb文件未require使用到的类,不提示错误。只有首个rb文件未require会有对应的报错信息,如果是rb文件再引入第二个rb文件,而第二个rb文件没有require对应的类,没有对应的报错信息,坑啊。
  • rb文件(非首个rb文件)中类名写错,rescue后报错信息在irb上输出一堆根本定义不到错误内容的信息,甚至错误发生在第几行也是定位不到。我就试过把YNHttp写成YnHttp,结果我花了很长时间在找bug,各种puts信息,血与泪的痛啊。T_T
  • 最坑的莫过于sublime不能debug,所以每一次找bug,都要花费我很长时间,不断的做重复工作。
  • 执行rb文件也麻烦,每一次都要到终端上cd到要执行的rb文件路径,再进入irb,再load要执行的rb文件。

本次源代码全部已放在我的Github上,路径:https://github.com/mia2002/yn_server

2 设计思想

我用笔粗略的画了整个逻辑执行过程,如下图所示:

首先,在接收Http请求和处理Http请求应在不同的线程中进行,线程1(接收请求)只负责接收Http请求,并把Http请求存放在队列中,线程2(处理Http请求)负责从队列中拿出请求,并对Http协议进行分析,提取路径和参数,分配到各自的task方法。其实这里就是使用到了生产者消费者模式。

本项目只使用了两条线程,其实更优的处理应该是处理Http请求的线程应根据服务器实际情况分配足够的线程数,并使用线程池管理所有线程,但是本次并没有对此进行优化。

3 HTTP协议

这里给网络知识忘记了或者压根就不知道的朋友稍微温习一下:

HTTP是Web浏览器和Web服务器之间通信的标准协议。HTTP指定客户端与服务器如何建立连接、客户端如何从服务器请求数据,服务器如何响应请求,以及最后如何关闭连接。

每个请求和响应都有同样的基本形式:一个首行、一个包含元数据的HTTP首部,然后是一个消息体。

GET请求:

GET /RubyServer/hello?name=yanyan&pwd=123 HTTP/1.1
Host: localhost:9000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) 
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
Connection: keep-alive

像这样的GET请求不包含消息体,所以请求以一个空行结束。

第一行称为请求行,包括一个方法(GET/POST)、资源路径以及HTTP版本。

POST请求

POST /RubyServer/json HTTP/1.1
Host: localhost:3000
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3)
Content-Length: 21

name=yan&password=123

在POST请求头中空行后接着是请求主体,Content-Length指明消息体有多少个字节。在处理post请求中一般是根据这个参数来提取主体内容。

那么综上,本次代码就是实现去分析Http请求头,通过第一行能判断我们的请求到底是GET还是POST,如果是GET请求的话,参数是放在资源路径?后面;如果是POST请求,则读取Content-Length的值,并通过该值提取主体内容。

当然,HTTP请求并不只有GET和POST请求,这里我们只处理这两种情况,有兴趣的童鞋可以去拓展一下。

4 代码分析

经过简单的HTTP知识温习后,相信接下来的代码分析应该很好理解。

1.yn_socket_queue.rb

这个类目前的设计只是简单封装了一下,为以后的功能拓展做准备。

class YNSocketQueue

    def initialize()
        @queue=Queue.new
    end

    def push(socket)
        raise "Illegal Argument, must be a TCPSocket Object!!" unless socket.is_a? TCPSocket
        if socket != nil
            @queue << socket
        end
    end

    def take
        @queue.pop
    end
end

2.yn_request.rb

此类实现将获取到的请求参数由"name=yan&password=123"转换为Hash,我也不清楚ruby的API到底有没有这个功能的实现,我是找遍了String类、Array类和Hash类,都没发现有类似的功能,于是就自己实现。

通过Request.new.get(key)可以实现根据参数名称获取参数的值

class YNRequest

    include Enumerable

    attr_reader :hash

    def initialize(content="")
        @hash = Hash.new
        if content != ""
            _arr = []
            _arr = content.include?("&") ? content.split("&") : _arr.push(content)
            _arr.each do |e|
                __arr = e.split("=")
                @hash[__arr[0].to_sym]=__arr[1]
            end
        end     
    end

    def get(key)
        @hash[key.to_sym]
    end

    def each
        raise 'please provide a block!' unless block_given?
        @hash.each do |e|
            yield e
        end
    end
end

3.yn_http.rb

此类的设计主要是用来返回Http响应文。

class YNHttp

    def initialize(_status=200,_server="Apache-Coyote/1.1",_pragma="no-cache",_control="no-cache",_content_type="text/json",_charset="UTF-8",_body="")
        @status = _status
        @server = _server
        @pragma = _pragma
        @cache_control = _control
        @content_type = _content_type
        @charset = _charset
        @body = _body
        @content_length = _body.length
    end

    @@status_hash={
        100 => "CONTINUE",
        200 => "OK",
        201 => "CREATED",
        202 => "ACCEPTED",
        204 => "NO CONTENT",
        302 => "MOVED TEMPORARILY",
        400 => "BAD REQUEST",
        401 => "UNAUTHORIZED",
        402 => "PAYMENT REQUIRED",
        403 => "FORBIDDEN",
        404 => "NOT FOUND",
        408 => "REQUEST TIMEOUT",
        409 => "CONFLICT",
        410 => "GONE",
        500 => "INTERNAL SERVER ERROR"
    }

    def status=(_status)
        @status=_status
    end

    def server=(_server)
        @server=_server
    end

    def pragma=(_pragma)
        @pragma=_pragma
    end

    def cache_control=(_control)
        @cache_control=_control
    end

    def content_type=(_content_type)
        @content_type=_content_type
    end

    def charset=(_charset)
        @charset=_charset
    end

    def body=(result)
        @content_length=result.size
        @body=result
    end

    def response
        "HTTP/1.1 #{@status} #{@@status_hash[@status]}\r\n" +
        "Server:#{@server}\r\n" + 
        "Pragma:#{@pragma}\r\n" + 
        "Cache-Control:#{@cache_control}\r\n" + 
        "Content-Type:#{@content_type};charset=#{@charset}\r\n" + 
        "Content-Length:#{@content_length}\r\n" + 
        "\r\n" + 
        "#{@body}"
    end
end

4.yn_route_util.rb

这个工具类主要是用来配置请求的资源路径对应Task中的方法名,HandlerRequest根据拿到的方法名动态执行Task中的方法。一开始我是考虑使用责任链来实现的,后面发现ruby有send这个方法可以动态执行类定义的方法,果断使用send来实现。

另外,本来原先设计是准备使用properties文件的,但是用gemspec打包后,不知道打包的properties文件到底被存放在什么路径下,试了好几个路径,都是提示file not found,知道怎么回事的大神们可以在评论或者发邮件告诉我,感激不尽!

# require 'yaml'

class YNRouteUtil

    include Enumerable

    @@route_hash = {
        "/RubyServer/hello" => "say_hello",
        "/RubyServer/json" => "test_json",
    }

    
    # def initialize
    #   @route_hash = Hash.new
    #   puts "route util initialize"
    #   _arr = YAML.load_file('route.properties').split(" ")
    #   puts "route_file: #{_arr}"
    #   _arr.each do |e|
    #       __arr = e.split("=")
    #       @route_hash[__arr[0]] = __arr[1]
    #   end

    # end

    def get_method(route)
        @@route_hash[route]
    end

    def each
        raise 'please provide a block!' unless block_given?
        @@route_hash.each do |e|
            yield e
        end
    end

end

5.yn_task.rb

这个类主要作用是用来处理请求的,本示例代码只提供了两个测试方法和一个默认方法,分别是say_hello(请求路径:/RubyServer/hello将执行该方法)、test_json(请求路径:/RubyServer/json将执行该方法)和default(请求路径找不到时执行该方法)。

以下这些方法对应的请求路径均在YNRouteUtil这个类中进行配置请求路径以及对应的YNTask类中的方法名,并在YNTask中添加该方法,实现对请求的处理,通过@request.get(key)可以实现根据接口定义的参数名称来获取参数的值,YNHttp.new.body="json string"返回处理后的json数据,最后,切记一定要返回最终的请求响应文,如test_json最后的http.response

require 'json'
require 'yn_http'
require 'yn_request'

class YNTask
    def initialize(request)
        @request = request
    end

    def method_missing(method_name)
        puts "#{method_name} not found in YNTask,please check yn_route_uril.rb"
        default
    end

    # route: /RubyServer/hello
    def say_hello
        begin
        http = YNHttp.new
        http.content_type = "text/html"
        _param = ""
        @request.each do |e|
            _param+="#{e[0]}=#{e[1]}<br>"
        end
        
        http.body = "<html><head><title>Hello to Ruby Server</title></html><body><h1>Hi,welcome to Yan's ruby server!</h1><p>the request param:<br>#{_param}</p></body></html>"
        # return the response result
        http.response
        rescue Exception => e
            puts e.send(:caller)
        end
        
    end

    # route: /RubyServer/json
    def test_json
        http = YNHttp.new
        http.content_type = "text/json"
        result = JSON.generate(@request.hash)
        http.body = result
        http.response # 最后必须返回影响文
    end

    # url not found
    def default
        http = YNHttp.new
        http.content_type = "text/html"
        http.status = 404
        http.body = "<html><head><title>Welcome</title></html><body><h1>Welcome to Yan's ruby server!</h1><p><h3>404 Not Found</h3></p></body></html>"
        http.response # 最后必须返回影响文
    end
end

6.yn_socket_server.rb

该类主要实现开启一个TCPServer服务,接收http请求,并将接收到的请求push到队列中。

require 'socket'
require 'yn_socket_queue'
require 'yn_handle_request'

class YNSocketServer

    # 初始化
    # port 端口号
    def initialize(port,queue)
        @port = port
        @queue = queue
    end

    def start_server
        begin
            @server=TCPServer.open(@port)
            puts "start successfully!!"
            loop{
                @client=@server.accept
                @queue.push(@client)
            }
        rescue Exception => e
            puts e.send(:caller)
        end
        
    end
end

7.yn_handle_request.rb

该类是做处理Http请求,是最最核心的类,也是业务逻辑最复杂的类。

它主要做的处理有如下:

  • 分析HTTP请求头
  • 根据资源路径查找YNTask中的方法名
  • 动态执行YNTask类中的方法,获取最终的响应文
  • 返回响应文给请求客户端

require 'yn_socket_queue'
require 'yn_request'
require 'yn_task'
require 'yn_route_util'

# 处理http请求
# create by yan
class YNHandleRequest

    def initialize(socket_queue)
        @socket_queue=socket_queue
    end

    def handle
        loop do
            begin
                client = @socket_queue.take
                puts "-----------------------------------"
                _method,path = client.gets.split
                puts "url: #{path}"
                puts "method: #{_method}"
                headers={}
                while line = client.gets.split(' ',2)
                    break if line[0]==""
                    headers[line[0].chop] = line[1].strip
                end
                data = ""
                servlet_url = ""
                if _method.upcase == 'POST'
                    data = client.read(headers["Content-Length"].to_i)
                    servlet_url = path
                elsif _method.upcase == 'GET'
                    if path.include? '?'
                        # 带参数
                        data = path[(path.index('?')+1)..path.length]
                        servlet_url = path[0...path.index('?')]
                    else
                        data = ""
                        servlet_url = path
                    end
                end
                request = YNRequest.new(data)
                puts "parameter: #{request.hash}"
                util = YNRouteUtil.new
                route = util.get_method(servlet_url)
                task = YNTask.new(request)
                route = "default" if route == nil || route.empty?
                puts "route: #{route}"
                _result = task.send(route) #动态执行方法
                client.write(_result)
            
            rescue Exception => e
                puts e.send(:caller)
            ensure
                client.close
            end
            
        end
    end
end

最后,我们可以分别开启两条线程来执行代码,一条线程主要用来负责接收Http请求,另外一条线程用来处理Http请求。

5 GEM打包

1.创建与项目同名的.gemspec文件,本例为:yn_server.gemspec,其实不同名也可以,但是为了便于管理,推荐还是建同名文件会比较好。

在.gemspec文件中写入如下内容:

Gem::Specification.new do |s|  
  s.name        = 'yn_server'  
  s.version     = '0.1'  
  s.date        = '2017-03-23'  
  s.summary     = 'Ruby Server!'
  s.description = 'A simple socket server gem'
  s.authors     = ['Yan Ng']  
  s.email       = 'yan@yerl.cn'  
  s.files       = %w(
                        lib/yn_handle_request.rb
                        lib/yn_http.rb
                        lib/yn_server.rb
                        lib/yn_request.rb
                        lib/yn_route_util.rb
                        lib/yn_socket_queue.rb
                        lib/yn_socket_server.rb
                        lib/yn_task.rb
                    )
  s.homepage    =  
    'http://rubygems.org/gems/yn_server'  
  s.license     = 'MIT'
end  

注意,s.name一定要是执行的第一个rb文件名称,在打包的rb文件一定要有和该名字一样的ruby文件,否则,最终安装后,require我们这个name,系统会报找不到文件。我就被坑过,最后还是谷歌出来的。

2.生成gem包

在终端执行gem build后会在当前路径生成gem包。

gem build yn_server.gemspec

3.install gem

生成的gem包可以实现在本地安装,使用gem install命令

sudo gem install yn_manage-0.1.gem 

通过gem list命令可以查看是否已经安装成功。

4.irb导入自己的gem包

通过gem install后我们就不需要再向从前那样使用load .rb文件,可以直接使用require引入要使用的类。

示例代码:

# 'yn_server'为gemspec中的s.name所配置的名称
require 'yn_server' 

5.发布gem包

我们可以把包发布到rubygems.org,这样其他人在引用我们的gem包时就可以直接使用gem install来安装。

1)首先得先到https://rubygems.org注册帐号,并完成邮件激活帐号。

2)命令行push gem

gem push yn_server-0.1.gem

等几分钟后,可以在rubygems.org上搜索到刚push的gem包信息。

3)命令行gem install

接下来,其他人要使用这个gem包,就可以直接打开终端输入命令:

sudo gem install yn_server

4)执行gem包

使用YNServer.start(port)启动服务,参数端口可不传,不带参数时默认端口号为2000。

打开浏览器输入地址:

http://localhost:2000/RubyServer/hello?name=yan&pwd=123

执行结果:

终端结果:

6 心得体会

虽说我对Ruby还有很多不懂的地方,但是作为一个新手来说,我坚持完成了一个简单的ruby项目编写,这当中学习到很多东西。

e-mail:yan@yerl.cn

blog:mia2002.cn

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容