├── go.mod ├── pkg └── fcgi │ ├── name_value.go │ ├── header.go │ ├── record.go │ ├── const.go │ ├── pool_test.go │ ├── pool.go │ ├── client.go │ ├── request.go │ └── client_test.go ├── LICENSE └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iwind/gofcgi 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /pkg/fcgi/name_value.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | type NameValuePair struct { 4 | NameLength uint16 5 | ValueLength uint16 6 | //NameData []byte 7 | //ValueData []byte 8 | } 9 | -------------------------------------------------------------------------------- /pkg/fcgi/header.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | type Header struct { 4 | Version byte 5 | Type byte 6 | RequestId uint16 7 | ContentLength uint16 8 | PaddingLength byte 9 | Reserved byte 10 | //ContentData []byte 11 | //PaddingData []byte 12 | } 13 | -------------------------------------------------------------------------------- /pkg/fcgi/record.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | type UnknownTypeBody struct { 4 | recordType byte 5 | reserved [7]byte 6 | } 7 | 8 | type BeginRequestBody struct { 9 | roleB1 byte 10 | roleB0 byte 11 | flags byte 12 | reserved [5]byte 13 | } 14 | 15 | type EndRequestBody struct { 16 | appStatusB3 byte 17 | appStatusB2 byte 18 | appStatusB1 byte 19 | appStatusB0 byte 20 | protocolStatus byte 21 | reserved [3]byte 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Liu Xiangchao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | a **golang client for fastcgi**, support connection pool and easy to use. 3 | 4 | # Pool usage 5 | ~~~golang 6 | // retrieve shared pool 7 | pool := fcgi.SharedPool("tcp", "127.0.0.1:9000", 16) 8 | client, err := pool.Client() 9 | if err != nil { 10 | return 11 | } 12 | 13 | // create a request 14 | req := fcgi.NewRequest() 15 | params := map[string]string{ 16 | "SCRIPT_FILENAME": "[PATH TO YOUR SCRIPT]/index.php", 17 | "SERVER_SOFTWARE": "gofcgi/1.0.0", 18 | "REMOTE_ADDR": "127.0.0.1", 19 | "QUERY_STRING": "NAME=VALUE", 20 | 21 | "SERVER_NAME": "example.com", 22 | "SERVER_ADDR": "127.0.0.1:80", 23 | "SERVER_PORT": "80", 24 | "REQUEST_URI": "/index.php", 25 | "DOCUMENT_ROOT": "[PATH TO YOUR SCRIPT]", 26 | "GATEWAY_INTERFACE": "CGI/1.1", 27 | "REDIRECT_STATUS": "200", 28 | "HTTP_HOST": "example.com", 29 | 30 | "REQUEST_METHOD": "POST", // for post method 31 | "CONTENT_TYPE": "application/x-www-form-urlencoded", // for post 32 | } 33 | 34 | req.SetTimeout(5 * time.Second) 35 | req.SetParams(params) 36 | 37 | // set request body 38 | r := bytes.NewReader([]byte("name=lu&age=20")) 39 | req.SetBody(r, uint32(r.Len())) 40 | 41 | // call request 42 | resp, err := client.Call(req) 43 | if err != nil { 44 | return 45 | } 46 | 47 | // read data from response 48 | data, err := ioutil.ReadAll(resp.Body) 49 | if err != nil { 50 | return 51 | } 52 | log.Println("resp body:", string(data)) 53 | ~~~ -------------------------------------------------------------------------------- /pkg/fcgi/const.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | // referer: http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html 4 | 5 | const ( 6 | // Listening socket file number 7 | FCGI_LISTENSOCK_FILENO = 0 8 | 9 | // Number of bytes in a FCGI_Header 10 | FCGI_HEADER_LEN = 8 11 | 12 | // Value for version component of FCGI_Header 13 | FCGI_VERSION_1 = 1 14 | 15 | // Values for type component of FCGI_Header 16 | FCGI_BEGIN_REQUEST = byte(1) 17 | FCGI_ABORT_REQUEST = byte(2) 18 | FCGI_END_REQUEST = byte(3) 19 | FCGI_PARAMS = byte(4) 20 | FCGI_STDIN = byte(5) 21 | FCGI_STDOUT = byte(6) 22 | FCGI_STDERR = byte(7) 23 | FCGI_DATA = byte(8) 24 | FCGI_GET_VALUES = byte(9) 25 | FCGI_GET_VALUES_RESULT = byte(10) 26 | FCGI_UNKNOWN_TYPE = byte(11) 27 | FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE 28 | 29 | // Value for requestId component of FCGI_Header 30 | FCGI_NULL_REQUEST_ID = 0 31 | 32 | // Mask for flags component of FCGI_BeginRequestBody 33 | FCGI_KEEP_CONN = byte(1) 34 | 35 | // Values for role component of FCGI_BeginRequestBody 36 | FCGI_RESPONDER = byte(1) 37 | FCGI_AUTHORIZER = byte(2) 38 | FCGI_FILTER = byte(3) 39 | 40 | // Values for protocolStatus component of FCGI_EndRequestBody 41 | FCGI_REQUEST_COMPLETE = byte(0) 42 | FCGI_CANT_MPX_CONN = byte(1) 43 | FCGI_OVERLOADED = byte(2) 44 | FCGI_UNKNOWN_ROLE = byte(3) 45 | 46 | // Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records 47 | FCGI_MAX_CONNS = "FCGI_MAX_CONNS" 48 | FCGI_MAX_REQS = "FCGI_MAX_REQS" 49 | FCGI_MPXS_CONNS = "FCGI_MPXS_CONNS" 50 | ) 51 | 52 | var PAD = [255]byte{} 53 | -------------------------------------------------------------------------------- /pkg/fcgi/pool_test.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestSharedPool(t *testing.T) { 11 | pool := SharedPool("tcp", "127.0.0.1:9000", 8) 12 | 13 | time.Sleep(100 * time.Millisecond) 14 | 15 | for i := 0; i < 3; i++ { 16 | go func() { 17 | client, err := pool.Client() 18 | if err != nil { 19 | t.Fatal(errors.New("client should not be nil")) 20 | } 21 | t.Logf("client:%p", client) 22 | 23 | req := NewRequest() 24 | req.KeepAlive() 25 | 26 | req.SetParams(map[string]string{ 27 | "SCRIPT_FILENAME": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/index.php", 28 | "SERVER_SOFTWARE": "gofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgi/1.0.0", 29 | "REMOTE_ADDR": "127.0.0.1", 30 | "QUERY_STRING": "name=value&__ACTION__=/@wx", 31 | 32 | "SERVER_NAME": "wx.balefm.cn", 33 | "SERVER_ADDR": "127.0.0.1:80", 34 | "SERVER_PORT": "80", 35 | "REQUEST_URI": "/index.php?__ACTION__=/@wx", 36 | "DOCUMENT_ROOT": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/", 37 | "GATEWAY_INTERFACE": "CGI/1.1", 38 | "REDIRECT_STATUS": "200", 39 | "HTTP_HOST": "wx.balefm.cn", 40 | 41 | "REQUEST_METHOD": "GET", 42 | }) 43 | 44 | resp, _, err := client.Call(req) 45 | if err != nil { 46 | t.Log(err.Error()) 47 | } else { 48 | bodyBytes, err := ioutil.ReadAll(resp.Body) 49 | if err != nil { 50 | t.Log(err.Error()) 51 | } else { 52 | t.Log(string(bodyBytes)) 53 | } 54 | } 55 | }() 56 | } 57 | 58 | time.Sleep(2 * time.Second) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/fcgi/pool.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | var pools = map[string]*Pool{} // fullAddress => *Pool 11 | var poolsLocker = sync.Mutex{} 12 | 13 | type Pool struct { 14 | size uint 15 | timeout time.Duration 16 | clients []*Client 17 | locker sync.Mutex 18 | } 19 | 20 | func SharedPool(network string, address string, size uint) *Pool { 21 | poolsLocker.Lock() 22 | defer poolsLocker.Unlock() 23 | 24 | fullAddress := network + "//" + address 25 | pool, found := pools[fullAddress] 26 | if found { 27 | return pool 28 | } 29 | 30 | if size == 0 { 31 | size = 8 32 | } 33 | 34 | pool = &Pool{ 35 | size: size, 36 | } 37 | 38 | for i := uint(0); i < size; i++ { 39 | client := NewClient(network, address) 40 | client.KeepAlive() 41 | 42 | // prepare one for first request, and left for async request 43 | if i == 0 { 44 | err := client.Connect() 45 | if err != nil { 46 | log.Println("[gofcgi]" + err.Error()) 47 | } 48 | } else { 49 | go func() { 50 | err := client.Connect() 51 | if err != nil { 52 | log.Println("[gofcgi]" + err.Error()) 53 | } 54 | }() 55 | } 56 | pool.clients = append(pool.clients, client) 57 | } 58 | 59 | // watch connections 60 | go func() { 61 | ticker := time.NewTicker(10 * time.Second) 62 | for range ticker.C { 63 | for _, client := range pool.clients { 64 | if !client.isAvailable { 65 | _ = client.Connect() 66 | } 67 | } 68 | } 69 | }() 70 | 71 | pools[fullAddress] = pool 72 | 73 | return pool 74 | } 75 | 76 | func (this *Pool) Client() (*Client, error) { 77 | this.locker.Lock() 78 | defer this.locker.Unlock() 79 | 80 | if len(this.clients) == 0 { 81 | return nil, errors.New("no available clients to use") 82 | } 83 | 84 | // find a free one 85 | for _, client := range this.clients { 86 | if client.isAvailable && client.isFree { 87 | return client, nil 88 | } 89 | } 90 | 91 | // find available on 92 | for _, client := range this.clients { 93 | if client.isAvailable { 94 | return client, nil 95 | } 96 | } 97 | 98 | // use first one 99 | err := this.clients[0].Connect() 100 | if err == nil { 101 | return this.clients[0], nil 102 | } 103 | 104 | return nil, errors.New("no available clients to use") 105 | } 106 | -------------------------------------------------------------------------------- /pkg/fcgi/client.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "net/http" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | var ErrClientDisconnect = errors.New("lost connection to server") 13 | 14 | type Client struct { 15 | isFree bool 16 | isAvailable bool 17 | 18 | keepAlive bool 19 | 20 | network string 21 | address string 22 | conn net.Conn 23 | 24 | locker sync.Mutex 25 | 26 | expireTime time.Time 27 | expireLocker sync.Mutex 28 | 29 | mock bool 30 | } 31 | 32 | func NewClient(network string, address string) *Client { 33 | client := &Client{ 34 | isFree: true, 35 | isAvailable: false, 36 | network: network, 37 | address: address, 38 | expireTime: time.Now().Add(86400 * time.Second), 39 | } 40 | 41 | // deal with expireTime 42 | go func() { 43 | for { 44 | time.Sleep(1 * time.Second) 45 | if time.Since(client.expireTime) > 0 { 46 | _ = client.conn.Close() 47 | 48 | client.expireLocker.Lock() 49 | client.expireTime = time.Now().Add(86400 * time.Second) 50 | client.expireLocker.Unlock() 51 | } 52 | } 53 | }() 54 | return client 55 | } 56 | 57 | func (this *Client) KeepAlive() { 58 | this.keepAlive = true 59 | } 60 | 61 | func (this *Client) Call(req *Request) (resp *http.Response, stderr []byte, err error) { 62 | this.isFree = false 63 | 64 | this.locker.Lock() 65 | 66 | if this.keepAlive && this.conn == nil { 67 | err := this.Connect() 68 | if err != nil { 69 | this.locker.Unlock() 70 | return nil, nil, err 71 | } 72 | } 73 | 74 | if this.keepAlive { 75 | req.keepAlive = true 76 | } 77 | 78 | defer func() { 79 | if this.mock { 80 | time.Sleep(1 * time.Second) 81 | } 82 | this.isFree = true 83 | this.locker.Unlock() 84 | }() 85 | 86 | if this.conn == nil { 87 | return nil, nil, errors.New("no connection to server") 88 | } 89 | 90 | if req.timeout > 0 { 91 | this.beforeTime(req.timeout) 92 | } 93 | resp, stderr, err = req.CallOn(this.conn) 94 | this.endTime() 95 | 96 | // if lost connection, retry 97 | if err != nil { 98 | log.Println("[gofcgi]" + err.Error()) 99 | 100 | if err == ErrClientDisconnect { 101 | // retry again 102 | this.Close() 103 | err = this.Connect() 104 | if err == nil { 105 | if req.timeout > 0 { 106 | this.beforeTime(req.timeout) 107 | } 108 | resp, stderr, err = req.CallOn(this.conn) 109 | this.endTime() 110 | } else { 111 | log.Println("[gofcgi]again:" + err.Error()) 112 | this.Close() 113 | } 114 | } 115 | } 116 | 117 | return resp, stderr, err 118 | } 119 | 120 | func (this *Client) Close() { 121 | this.isAvailable = false 122 | if this.conn != nil { 123 | _ = this.conn.Close() 124 | } 125 | this.conn = nil 126 | } 127 | 128 | func (this *Client) Connect() error { 129 | this.isAvailable = false 130 | 131 | // @TODO set timeout 132 | conn, err := net.Dial(this.network, this.address) 133 | if err != nil { 134 | log.Println("[gofcgi]" + err.Error()) 135 | return err 136 | } 137 | 138 | this.conn = conn 139 | this.isAvailable = true 140 | 141 | return nil 142 | } 143 | 144 | func (this *Client) Mock() { 145 | this.mock = true 146 | } 147 | 148 | func (this *Client) beforeTime(timeout time.Duration) { 149 | this.expireLocker.Lock() 150 | this.expireTime = time.Now().Add(timeout) 151 | this.expireLocker.Unlock() 152 | } 153 | 154 | func (this *Client) endTime() { 155 | this.expireLocker.Lock() 156 | this.expireTime = time.Now().Add(86400 * time.Second) 157 | this.expireLocker.Unlock() 158 | } 159 | -------------------------------------------------------------------------------- /pkg/fcgi/request.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math" 12 | "net" 13 | "net/http" 14 | "regexp" 15 | "strconv" 16 | "sync" 17 | "time" 18 | ) 19 | 20 | var currentRequestId = uint16(0) 21 | var requestIdLocker = sync.Mutex{} 22 | 23 | var statusLineRegexp = regexp.MustCompile("^HTTP/[.\\d]+ \\d+") 24 | var statusSplitRegexp = regexp.MustCompile("^(\\d+)\\s+") 25 | var contentLengthRegexp = regexp.MustCompile("^\\d+$") 26 | 27 | // Request Referer: 28 | // - FastCGI Specification: http://www.mit.edu/~yandros/doc/specs/fcgi-spec.html 29 | type Request struct { 30 | id uint16 31 | keepAlive bool 32 | timeout time.Duration 33 | params map[string]string 34 | body io.Reader 35 | bodyLength uint32 36 | } 37 | 38 | func NewRequest() *Request { 39 | req := &Request{} 40 | req.id = req.nextId() 41 | req.keepAlive = false 42 | return req 43 | } 44 | 45 | func (this *Request) KeepAlive() { 46 | this.keepAlive = true 47 | } 48 | 49 | func (this *Request) SetParams(params map[string]string) { 50 | this.params = params 51 | } 52 | 53 | func (this *Request) SetParam(name, value string) { 54 | this.params[name] = value 55 | } 56 | 57 | func (this *Request) SetBody(body io.Reader, length uint32) { 58 | this.body = body 59 | this.bodyLength = length 60 | } 61 | 62 | func (this *Request) SetTimeout(timeout time.Duration) { 63 | this.timeout = timeout 64 | } 65 | 66 | func (this *Request) CallOn(conn net.Conn) (resp *http.Response, stderr []byte, err error) { 67 | err = this.writeBeginRequest(conn) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | err = this.writeParams(conn) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | 77 | err = this.writeStdin(conn) 78 | if err != nil { 79 | return nil, nil, err 80 | } 81 | 82 | return this.readStdout(conn) 83 | } 84 | 85 | func (this *Request) writeBeginRequest(conn net.Conn) error { 86 | flags := byte(0) 87 | if this.keepAlive { 88 | flags = FCGI_KEEP_CONN 89 | } 90 | role := FCGI_RESPONDER 91 | data := [8]byte{byte(role >> 8), byte(role), flags} 92 | return this.writeRecord(conn, FCGI_BEGIN_REQUEST, data[:]) 93 | } 94 | 95 | func (this *Request) writeParams(conn net.Conn) error { 96 | // 检查CONTENT_LENGTH 97 | if this.body != nil { 98 | contentLength, found := this.params["CONTENT_LENGTH"] 99 | if !found || !contentLengthRegexp.MatchString(contentLength) { 100 | if this.bodyLength > 0 { 101 | this.params["CONTENT_LENGTH"] = fmt.Sprintf("%d", this.bodyLength) 102 | } else { 103 | return errors.New("[fcgi]'CONTENT_LENGTH' should be specified") 104 | } 105 | } 106 | } 107 | 108 | for name, value := range this.params { 109 | buf := bytes.NewBuffer([]byte{}) 110 | 111 | b := make([]byte, 8) 112 | binary.BigEndian.PutUint32(b, uint32(len(name))|1<<31) 113 | buf.Write(b[:4]) 114 | 115 | binary.BigEndian.PutUint32(b, uint32(len(value))|1<<31) 116 | buf.Write(b[:4]) 117 | 118 | buf.WriteString(name) 119 | buf.WriteString(value) 120 | 121 | err := this.writeRecord(conn, FCGI_PARAMS, buf.Bytes()) 122 | if err != nil { 123 | //log.Println("[fcgi]write params error:", err.Error()) 124 | return err 125 | } 126 | } 127 | 128 | // write end 129 | return this.writeRecord(conn, FCGI_PARAMS, []byte{}) 130 | } 131 | 132 | func (this *Request) writeStdin(conn net.Conn) error { 133 | if this.body != nil { 134 | // read body with buffer 135 | buf := make([]byte, 60000) 136 | for { 137 | n, err := this.body.Read(buf) 138 | 139 | if n > 0 { 140 | err := this.writeRecord(conn, FCGI_STDIN, buf[:n]) 141 | if err != nil { 142 | return err 143 | } 144 | } 145 | 146 | if err != nil { 147 | break 148 | } 149 | } 150 | } 151 | 152 | return this.writeRecord(conn, FCGI_STDIN, []byte{}) 153 | } 154 | 155 | func (this *Request) writeRecord(conn net.Conn, recordType byte, contentData []byte) error { 156 | contentLength := len(contentData) 157 | 158 | // write header 159 | header := &Header{ 160 | Version: FCGI_VERSION_1, 161 | Type: recordType, 162 | RequestId: this.id, 163 | ContentLength: uint16(contentLength), 164 | PaddingLength: byte(-contentLength & 7), 165 | } 166 | 167 | buf := bytes.NewBuffer([]byte{}) 168 | err := binary.Write(buf, binary.BigEndian, header) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | _, err = io.Copy(conn, buf) 174 | if err != nil { 175 | return ErrClientDisconnect 176 | } 177 | 178 | // write data 179 | _, err = conn.Write(contentData) 180 | if err != nil { 181 | return ErrClientDisconnect 182 | } 183 | 184 | // write padding 185 | _, err = conn.Write(PAD[:header.PaddingLength]) 186 | if err != nil { 187 | return ErrClientDisconnect 188 | } 189 | 190 | return nil 191 | } 192 | 193 | func (this *Request) readStdout(conn net.Conn) (resp *http.Response, stderr []byte, err error) { 194 | stdout := []byte{} 195 | 196 | for { 197 | respHeader := Header{} 198 | err := binary.Read(conn, binary.BigEndian, &respHeader) 199 | if err != nil { 200 | return nil, nil, ErrClientDisconnect 201 | } 202 | 203 | // check request id 204 | if respHeader.RequestId != this.id { 205 | continue 206 | } 207 | 208 | b := make([]byte, respHeader.ContentLength+uint16(respHeader.PaddingLength)) 209 | err = binary.Read(conn, binary.BigEndian, &b) 210 | if err != nil { 211 | log.Println("err:", err.Error()) 212 | return nil, nil, ErrClientDisconnect 213 | } 214 | 215 | if respHeader.Type == FCGI_STDOUT { 216 | stdout = append(stdout, b[:respHeader.ContentLength]...) 217 | continue 218 | } 219 | 220 | if respHeader.Type == FCGI_STDERR { 221 | stderr = append(stderr, b[:respHeader.ContentLength]...) 222 | continue 223 | } 224 | 225 | if respHeader.Type == FCGI_END_REQUEST { 226 | break 227 | } 228 | } 229 | 230 | if len(stdout) > 0 { 231 | statusStdout := []byte{} 232 | firstLineIndex := bytes.IndexAny(stdout, "\n\r") 233 | foundStatus := false 234 | if firstLineIndex >= 0 { 235 | firstLine := stdout[:firstLineIndex] 236 | if statusLineRegexp.Match(firstLine) { 237 | foundStatus = true 238 | statusStdout = stdout 239 | } 240 | } 241 | 242 | if !foundStatus { 243 | statusStdout = append([]byte("HTTP/1.0 200 OK\r\n"), stdout...) 244 | } 245 | resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(statusStdout)), nil) 246 | 247 | if err != nil { 248 | return nil, stderr, err 249 | } 250 | 251 | if !foundStatus { 252 | status := resp.Header.Get("Status") 253 | if len(status) > 0 { 254 | matches := statusSplitRegexp.FindStringSubmatch(status) 255 | if len(matches) > 0 { 256 | resp.Status = status 257 | 258 | statusCode, err := strconv.Atoi(matches[1]) 259 | if err != nil { 260 | resp.StatusCode = 200 261 | } else { 262 | resp.StatusCode = statusCode 263 | } 264 | } 265 | } 266 | } 267 | 268 | return resp, stderr, nil 269 | } 270 | 271 | if len(stderr) > 0 { 272 | return nil, stderr, errors.New("fcgi:" + string(stderr)) 273 | } 274 | 275 | return nil, stderr, errors.New("no response from server") 276 | } 277 | 278 | func (this *Request) nextId() uint16 { 279 | requestIdLocker.Lock() 280 | defer requestIdLocker.Unlock() 281 | 282 | currentRequestId++ 283 | 284 | if currentRequestId == math.MaxUint16 { 285 | currentRequestId = 0 286 | } 287 | 288 | return currentRequestId 289 | } 290 | -------------------------------------------------------------------------------- /pkg/fcgi/client_test.go: -------------------------------------------------------------------------------- 1 | package fcgi 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "strings" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestClientGet(t *testing.T) { 13 | client := &Client{ 14 | network: "tcp", 15 | address: "127.0.0.1:9000", 16 | } 17 | err := client.Connect() 18 | if err != nil { 19 | t.Fatal("connect err:", err.Error()) 20 | } 21 | 22 | req := NewRequest() 23 | req.SetParams(map[string]string{ 24 | "SCRIPT_FILENAME": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/index.php", 25 | "SERVER_SOFTWARE": "gofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgi/1.0.0", 26 | "REMOTE_ADDR": "127.0.0.1", 27 | "QUERY_STRING": "name=value&__ACTION__=/@wx", 28 | 29 | "SERVER_NAME": "wx.balefm.cn", 30 | "SERVER_ADDR": "127.0.0.1:80", 31 | "SERVER_PORT": "80", 32 | "REQUEST_URI": "/index.php?__ACTION__=/@wx", 33 | "DOCUMENT_ROOT": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/", 34 | "GATEWAY_INTERFACE": "CGI/1.1", 35 | "REDIRECT_STATUS": "200", 36 | "HTTP_HOST": "wx.balefm.cn", 37 | 38 | "REQUEST_METHOD": "GET", 39 | }) 40 | 41 | resp, stderr, err := client.Call(req) 42 | if err != nil { 43 | t.Fatal("do error:", err.Error()) 44 | } 45 | 46 | if len(stderr) > 0 { 47 | t.Fatal("stderr:", string(stderr)) 48 | } 49 | 50 | t.Log("resp, status:", resp.StatusCode) 51 | t.Log("resp, status message:", resp.Status) 52 | t.Log("resp headers:", resp.Header) 53 | 54 | data, err := ioutil.ReadAll(resp.Body) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | t.Log("resp body:", string(data)) 59 | } 60 | 61 | func TestClientGetAlive(t *testing.T) { 62 | client := &Client{ 63 | network: "tcp", 64 | address: "127.0.0.1:9000", 65 | } 66 | client.KeepAlive() 67 | err := client.Connect() 68 | if err != nil { 69 | t.Fatal("connect err:", err.Error()) 70 | } 71 | 72 | for i := 0; i < 10; i++ { 73 | req := NewRequest() 74 | req.SetParams(map[string]string{ 75 | "SCRIPT_FILENAME": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/index.php", 76 | "SERVER_SOFTWARE": "gofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgi/1.0.0", 77 | "REMOTE_ADDR": "127.0.0.1", 78 | "QUERY_STRING": "name=value&__ACTION__=/@wx", 79 | 80 | "SERVER_NAME": "wx.balefm.cn", 81 | "SERVER_ADDR": "127.0.0.1:80", 82 | "SERVER_PORT": "80", 83 | "REQUEST_URI": "/index.php?__ACTION__=/@wx", 84 | "DOCUMENT_ROOT": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/", 85 | "GATEWAY_INTERFACE": "CGI/1.1", 86 | "REDIRECT_STATUS": "200", 87 | "HTTP_HOST": "wx.balefm.cn", 88 | 89 | "REQUEST_METHOD": "GET", 90 | }) 91 | 92 | resp, _, err := client.Call(req) 93 | if err != nil { 94 | t.Fatal("do error:", err.Error()) 95 | } 96 | 97 | t.Log("resp, status:", resp.StatusCode) 98 | t.Log("resp, status message:", resp.Status) 99 | t.Log("resp headers:", resp.Header) 100 | 101 | data, err := ioutil.ReadAll(resp.Body) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | t.Log("resp body:", string(data)) 106 | 107 | time.Sleep(1 * time.Second) 108 | } 109 | } 110 | 111 | func TestClientPost(t *testing.T) { 112 | client := &Client{ 113 | network: "tcp", 114 | address: "127.0.0.1:9000", 115 | } 116 | err := client.Connect() 117 | if err != nil { 118 | t.Fatal("connect err:", err.Error()) 119 | } 120 | 121 | req := NewRequest() 122 | req.SetParams(map[string]string{ 123 | "SCRIPT_FILENAME": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/index.php", 124 | "SERVER_SOFTWARE": "gofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgigofcgi/1.0.0", 125 | "REMOTE_ADDR": "127.0.0.1", 126 | "QUERY_STRING": "name=value&__ACTION__=/@wx", 127 | 128 | "SERVER_NAME": "wx.balefm.cn", 129 | "SERVER_ADDR": "127.0.0.1:80", 130 | "SERVER_PORT": "80", 131 | "REQUEST_URI": "/index.php?__ACTION__=/@wx", 132 | "DOCUMENT_ROOT": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/", 133 | "GATEWAY_INTERFACE": "CGI/1.1", 134 | "REDIRECT_STATUS": "200", 135 | "HTTP_HOST": "wx.balefm.cn", 136 | 137 | "REQUEST_METHOD": "POST", 138 | "CONTENT_TYPE": "application/x-www-form-urlencoded", 139 | }) 140 | 141 | r := bytes.NewReader([]byte("name12=value&hello=world&name13=value&hello4=world")) 142 | //req.SetParam("CONTENT_LENGTH", fmt.Sprintf("%d", r.Len())) 143 | req.SetBody(r, uint32(r.Len())) 144 | 145 | resp, _, err := client.Call(req) 146 | if err != nil { 147 | t.Fatal("do error:", err.Error()) 148 | } 149 | 150 | t.Log("resp, status:", resp.StatusCode) 151 | t.Log("resp, status message:", resp.Status) 152 | t.Log("resp headers:", resp.Header) 153 | 154 | data, err := ioutil.ReadAll(resp.Body) 155 | if err != nil { 156 | t.Fatal(err) 157 | } 158 | t.Log("resp body:", string(data)) 159 | } 160 | 161 | func TestClientPerformance(t *testing.T) { 162 | threads := 100 163 | countRequests := 200 164 | countSuccess := 0 165 | countFail := 0 166 | locker := sync.Mutex{} 167 | beforeTime := time.Now() 168 | wg := sync.WaitGroup{} 169 | wg.Add(threads) 170 | 171 | pool := SharedPool("tcp", "127.0.0.1:9000", 16) 172 | 173 | for i := 0; i < threads; i++ { 174 | go func(i int) { 175 | defer wg.Done() 176 | 177 | for j := 0; j < countRequests; j++ { 178 | client, err := pool.Client() 179 | if err != nil { 180 | t.Fatal("connect err:", err.Error()) 181 | } 182 | 183 | req := NewRequest() 184 | req.SetTimeout(5 * time.Second) 185 | req.SetParams(map[string]string{ 186 | "SCRIPT_FILENAME": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/index.php", 187 | "SERVER_SOFTWARE": "gofcgi/1.0.0", 188 | "REMOTE_ADDR": "127.0.0.1", 189 | "QUERY_STRING": "name=value&__ACTION__=/@wx", 190 | 191 | "SERVER_NAME": "wx.balefm.cn", 192 | "SERVER_ADDR": "127.0.0.1:80", 193 | "SERVER_PORT": "80", 194 | "REQUEST_URI": "/index.php?__ACTION__=/@wx", 195 | "DOCUMENT_ROOT": "/Users/liuxiangchao/Documents/Projects/pp/apps/baleshop.ppk/", 196 | "GATEWAY_INTERFACE": "CGI/1.1", 197 | "REDIRECT_STATUS": "200", 198 | "HTTP_HOST": "wx.balefm.cn", 199 | 200 | "REQUEST_METHOD": "GET", 201 | }) 202 | 203 | resp, _, err := client.Call(req) 204 | if err != nil { 205 | locker.Lock() 206 | countFail++ 207 | locker.Unlock() 208 | continue 209 | } 210 | 211 | if resp.StatusCode == 200 { 212 | data, err := ioutil.ReadAll(resp.Body) 213 | if err != nil || strings.Index(string(data), "Welcome") == -1 { 214 | locker.Lock() 215 | countFail++ 216 | locker.Unlock() 217 | } else { 218 | locker.Lock() 219 | countSuccess++ 220 | locker.Unlock() 221 | } 222 | } else { 223 | locker.Lock() 224 | countFail++ 225 | locker.Unlock() 226 | } 227 | } 228 | }(i) 229 | } 230 | 231 | wg.Wait() 232 | 233 | t.Log("success:", countSuccess, "fail:", countFail, "qps:", int(float64(countSuccess+countFail)/time.Since(beforeTime).Seconds())) 234 | } 235 | --------------------------------------------------------------------------------