Programming + Go tapping TCP socket (part I)
We will introduce the low -level API of GO language that will be useful for practical application development from this series that uses GO language to experience the world of low -level programming.The first subject is socket communication, which is particularly commonly used in applications using networks.
However, there are not many opportunities to handle sockets directly with usually programming.So, in this article, we will look at how to use the socket with GO, using the familiar HTTP on the Web.Let's learn how to use sockets in GO language through HTTP.
Protocol and layer
The first is about the basics of network communication.
In order to communicate, it is necessary to share the rules of communication between the sender and the receiving side.This rule is called "protocol" (communication rules).
Protocols are usually used in combination according to their roles.Those who have taken information processing tests have heard the words "OSI 7 Hierarchy" or "TCP/IP reference model".These are divided into a hierarchy (layer) of various functions to realize network communication, and defines protocols that are responsible for each layer.
What is used in Internet communication is the TCP/IP reference model.The TCP/IP reference model is the following layer division.
レイヤーの名称 | 代表的なプロトコル |
---|---|
アプリケーション層 | HTTP |
トランスポート層 | TCP/UDP/QUIC |
インターネット層 | IP |
リンク層 | Wi-Fi、イーサネット |
Of these, only the layers above the transport layer need to be concerned to create an application.In the actual Internet communication, data is exchanged in the form of an IP packet through cables and wireless, but the application does not make direct IP packets.If you communicate according to the rules determined at the HTTP or TCP level, you can interact with the application on the other side of the network without worrying about the details required for the layer below.
Go言語では、HTTP、TCP、UDPについて組み込みの機能が提供されています1。 実用的なアプリケーションでは、それらの機能を使って、自分のアプリケーションに必要なプロトコルを実装していくことになります。
HTTP and protocols above it
The goal of this article is to use a low -level socket from a GO language for a program that handles networks, but in recent years you can use a convenient library and framework that can be used without knowing low -level sockets.increasing.For this reason, there are many stories that need to be known about the application layer, which is a high -ranking layer.
The most commonly used application layer protocol in modern Internet is the HTTP used on the web.So, first of all, we will explain the HTTP mechanism, then pick up some stories about the top layers.
Basics of HTTP
HTTP has a version of http/1.0 and http/1.1, and even HTTP/2, but here are the basic exchanges using http/1.0 as an example.
HTTP stipulates requests from clients and server response to it.In http/1.0, the client sends the following content to the server in text.
メソッド パス HTTP/1.0ヘッダ1: ヘッダーの値ヘッダ2: ヘッダーの値(空行)リクエストボディ(あれば)
In HTTP, some types of requirements for server are determined in advance.This is called the HTTP method.The HTTP method comes to the first line at the request.HTTP methods include GET and POST.
また、HTTPでは改行が区切り文字と決められています。 第4回で触れたように、読み込みは内容を分析しながら行う必要があるためコードが複雑になり、その分だけ処理が遅くなりがちです。 そこで現在のHTTPでは、リクエストの解析が簡単に行えるように、改行を区切り文字と決めているのです2。
When the server receives the above request, the resource stored in the specified path is returned as the following formal response:
HTTP/1.0 200 OKヘッダ1: ヘッダーの値ヘッダ2: ヘッダーの値(空行)サーバレスポンス
The number "200" in the first line is a code that represents the type of response.This is also defined by HTTP specifications, and the code that indicates the success of the response is 200.
HTTP was originally a protocol for sending and receiving documents for information exchange, but has been used by many non -software experts and researchers along with the Internet boom.It is an indispensable infrastructure now.
しかし、用途が広がったことでHTTPに対して要求される機能も増えました。 フォームを使って情報を送信できるようになったり、TLS3で通信の暗号を守る機能が入ったり、部分的なコンテンツをサーバから取得するXMLHttpRequestやFetch APIが入ったり、WebWorkerでマルチスレッド機能が入ったり、ServiceWorkerでオフライン動作もできるアプリケーション化が進んだりして現在に至っています。 魔改造に魔改造が加えられた結果、現在ではかなり複雑な仕組みになっています。
HTTP/2では通信内容がバイナリ化されて高速化しました。 ただし、ブラウザから見た通信内容の意味(セマンティクス)はHTTP/1.1と変化はありません。 規格上も、HTTP/2の規格はバイナリ表現の紹介に限定されています。 ここで紹介したのは古き良き1.0/1.1ですが、通信プロトコルとしての基本は今でも同じです4。
RPC
RPC (REMOTE PROCEDURE CALLING) is a mechanism that easily calls various functions provided by the server like a function on a local computer.For more than 10 years, it has been used as a protocol on HTTP, such as XML-RPC and JSON-RPC.The function call "passing and executing the argument and receiving the return value" is realized on the Internet as it is.
XML-RPC is provided as a standard library in Ruby, Python and PHP.JSON-RPC is also provided as a standard library in GO language.
JSON-RPCでは、プロトコルバージョン("jsonrpc"
キーの値)、メソッド("method"
)、引数("params"
)、送受信の対応を取るためのIDの4項目を使ってリクエストを送信します。 レスポンスのデータ構造はメソッドと引数の代わりに返り値("result"
)を設定します。 あとは決まったURLにHTTPのPOSTメソッドで送信するだけです。
// 送信側{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}// 受信側{"jsonrpc": "2.0", "result": 19, "id": 1}
REST
Web services are increasingly adopting APIs that are regarded as a hierarchical resource (like file) and use URL to acquire and post them.The style of consolidating communication between server clients into simple file servers with such APIs is called REST (Represential State Transfer).REST is a protocol that maximizes HTTP rules.
RESTの思想にしたがうシステムのことを「RESTful」といいます5。 最近では、RESTfulの究極形態(第4形態)として位置づけられる「HATEOAS」という考え方も広まりつつあります。 サーバからのHTTPレスポンスに「リンク」情報を入れ、そこから賢いクライアントプログラムが自律的にデータ探索をして情報を見つけられる世界を実現しようというものです。 人間がウェブサイトを見るときはページ内のリンクをたどって関連ページをたどっていきますが、その考え方を取り入れたRESTがHATEOASだといえるでしょう。
GitHub is a web service that adopts APIs according to the principle of HATEOAS.The "Issue" on GitHub is structured in a hierarchy called repository owner, repository, and Issue.Please access the next URL with a browser.
It contains information on isSue, and you can see that there are many other URL information.Hateoas is a common way to handle these URLs as paging information.In fact, if you look at the browser developer tools, HTTP's response contains the following headers.
Link: ; rel="next", ; rel="last"
Graphql
RPC -based Graphql is also attracting attention on a slightly different route from RESTFUL.This is a protocol of the application layer proposed by Facebook.
アプリケーションのすべての情報がきれいな階層構造にマッピングできるわけではありません。 FacebookなどのSNSではグループを作ることができますが、グループに所属しているユーザ、ユーザが所属しているグループなど、リソース表現は複数考えられます。 属性が増えれば増えるほど、それらの表現は複雑になっていきます。 Graphqlは、複数の属性から構成されている要素をピンポイントで取得するためのクエリー言語として機能します。 Graphqlで要素を取得するには、JavaScriptに似た構文({
と}
)を使って階層を表し、( )
を使って制約を指定します。
Although I do not know at this time as a specific case of services using Graphql, the next site offers a service that can be accessed by wrapping an existing REST API and accessing it with GRAPHQL.
Graphql is also a protocol of the application layer that can be used on HTTP.When dealing with the GET method of HTTP, the text of Graphql is given as a query as it is.When sending by POST, convert it to an equivalent JSON and then send it.
RFC, which can be said to be a system call in protocol
Most of the communication protocols handled on the Internet are defined in the RFC standard compiled by an IETF organization.
HTTP is of course RFC here.XML-RPC and JSON-RPC are not RFC, but there are protocols containing XML-RPC (RFC-3529).REST states that REST's advocate is one of the author of HTTP/1.1, and should first understand HTTP/1.1.Graphql is currently aiming for RFC.
In RFC, the emphasis is on whether communication can be performed well beyond the OS and equipment differences. Therefore, if you try to implement a code that handles a protocol or examine the specifications of the protocol, you often hit the RFC. In this series, we focus on the functions provided by the OS as a low -layer of programs, but in the communication program, this RFC is the layer that corresponds to the system call. The original text is all English, so it is difficult to read at first, but the writing style is ruled, and there is no joke that is difficult to interpret even if you look at the dictionary, and it is easier to read as a sentence. If you are interested, please try RFC reading. It is good compatibility with the new Google translation.
What is a socket?
When trying to create an application that uses the Internet, it is likely to be directly affected by coding in the application layer protocol as introduced in the previous section.GO language also incorporates HTTP functions, so you can develop applications using HTTP and protocols on it by using that API.
So, what kind of mechanism does HTTP use a lower layer?Currently, most OS uses a socket mechanism as an API when using a transport layer protocol from the application layer.
Generally, communication with other applications is called inter -process communication (IPC: Inter Process Communication).The OS has many inter -process communication functions, including signals, messaging, pipes, and shared memory.Sockets are also a kind of inter -process communication.The difference is that the socket is slightly different from other inter -process communication is that if the address and port number are divided, communication can be performed not only in the local computer but also with external computers.
Internet communication between applications is also performed through this socket.For example, in the HTTP communication using a normal browser, the inter -processed socket communication is performed on the server TCP port 80.
Here, let's realize HTTP communication using only TCP functions (net.conn) incorporated in GO language.
Basic structure of socket communication
The basic configuration for any socket communication is as follows.
Go言語の場合、サーバが呼ぶのはListen()
メソッド、クライアントが呼ぶのはDial()
メソッドというAPIの命名ルールが決まっており、ソケット通信でも同様です。
通信の手順はプロトコルによって異なります。 一方的な送信しかできないUDPのようなプロトコルもあれば、接続時にサーバがクライアントを認知(Accept()
)して双方向にやり取りができるようになるTCPやUnixドメインソケットなどのプロトコルもあります。
The life cycle of communication using a TCP socket in GO language is summarized as shown in the following figure (in this figure, the communication is cut from the server, but it can also be cut from the client).
net.Conn
は、io.Reader
、io.Writer
、io.Closer
にタイムアウトを設定するメソッドを追加したインタフェースで、通信のための共通インタフェースとして定義されています。
通信が確立できると、送信側、受信側の両方に、相手との通信を行うnet.Conn
インタフェースを満たすオブジェクトが渡ってきます。 以降はこのオブジェクトを使って通信を行います。
The TCP client code is as follows.
conn, err := net.Dial("tcp", "localhost:8080")if err != nil { panic(err)}// connを使った読み書き
The minimum code on the server side is as follows.
ln, err := net.Listen("tcp", ":8080")if err != nil { panic(err)}conn, err := ln.Accept()if err != nil { // handle error}// connを使った読み書き
You can respond as a server, but this code will end once accessed.Therefore, practical code does not look like this.
The person who implements the server using GO language is that the number of responses that can be processed in seconds is extremely high.You want to accept other requests during one request, or do tasks in parallel as long as the CPU allows.The realistic minimum server that meets these needs is the following code:
ln, err := net.Listen("tcp", ":8080")if err != nil { panic(err)}// 一度で終了しないためにAccept()を何度も繰り返し呼ぶfor { conn, err := ln.Accept() if err != nil { // handle error } // 1リクエスト処理中に他のリクエストのAccept()が行えるように // Goroutineを使って非同期にレスポンスを処理する go func() { // connを使った読み書き }()}
Implement an HTTP server in Go language
In the GO language, communication using TCP sockets is now possible, but what is important is what to do with this server.Even if you just return the text sent from the client, it will be a study of the network program, but since I introduced the basics of HTTP in the previous section, let's write an HTTP server here.
実際にGo言語でHTTPのコードを作成するときは、net/http
以下の高機能なAPIを使います。 低レベルなnet
パッケージのAPIを直接触って通信を行うことはほとんどありませんが、ここではソケット通信の実例として低レベルの機能を使ってHTTPサーバを書いていきます。
HTTP server using TCP socket
Let's realize HTTP/1.0 equivalent transmission and reception using GO language sockets.First is the server code.
package mainimport ( "bufio" "fmt" "io/ioutil" "net" "net/http" "net/http/httputil" "strings")func main() { listener, err := net.Listen("tcp", "localhost:8888") if err != nil { panic(err) } fmt.Println("Server is running at localhost:8888") for { conn, err := listener.Accept() if err != nil { panic(err) } go func() { fmt.Printf("Accept %v\n", conn.RemoteAddr()) // リクエストを読み込む request, err := http.ReadRequest( bufio.NewReader(conn)) if err != nil { panic(err) } dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) // レスポンスを書き込む response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 0, Body: ioutil.NopCloser( strings.NewReader("Hello World\n")), } response.Write(conn) conn.Close() }() }}
少し長いですが、基本構成は先ほど紹介したソケット通信そのままです。go func()
の中は非同期実行されます。
このサーバを起動して、同じマシンからcurlコマンドやウェブブラザを使ってlocalhost:8888
にアクセスしてみてください。 画面に"Hello World"と表示されたでしょうか?
開発者ツールなどでレスポンスの内容を見ると、次のようなログが出力されているはずです(User-Agent
の情報はクライアントによって異なります)。
Accept 127.0.0.1:54017GET / HTTP/1.1Host: localhost:8888Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Encoding: gzip, deflate, sdch, brAccept-Language: en-US,en;q=0.8,ja;q=0.6Connection: keep-aliveUpgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36
Let's explain the server code.
まずはクライアントから送られてきたリクエストの読み込みです。 自分でテキストを解析してもいいのですが、http.ReadRequest()
関数を使ってHTTPリクエストのヘッダー、メソッド、パスなどの情報を切り出しています。
読み込んだリクエストは、httpuitl.DumpRequest()
関数を使って取り出しています。 この関数はhttputil
以下にある便利なデバッグ用の関数です。 ここまでで、io.Reader
からバイト列を読み込んで分析してデバッグ出力に出す、という処理を行っています。
次は、HTTPリクエストを送信してくれたクライアント向けにレスポンスを生成するコードです。 これにはhttp.Response
構造体を使います。http.Response
構造体はWrite()
メソッドを持っているので、作成したレスポンスのコンテンツをio.Writer
に直接書き込むことができます。
That's all for the server code.Each process is not so difficult.It is not difficult to create a web server using a raw TCP socket in GO language.However, it has become much longer than the code introduced in the second Internet access.
HTTP client using TCP socket
Let's make an HTTP client.
package mainimport ( "bufio" "fmt" "net" "net/http" "net/http/httputil")func main() { conn, err := net.Dial("tcp", "localhost:8888") if err != nil { panic(err) } request, err := http.NewRequest( "GET", "http://localhost:8888", nil) if err != nil { panic(err) } request.Write(conn) response, err := http.ReadResponse( bufio.NewReader(conn), request) if err != nil { panic(err) } dump, err := httputil.DumpResponse(response, true) if err != nil { panic(err) } fmt.Println(string(dump))}
Actually, the shortest code using the socket has been introduced in the second article in the series.
conn, err := net.Dial("tcp", "ascii.jp:80")if err != nil { panic(err)}conn.Write([]byte("GET / HTTP/1.0\r\nHost: ascii.jp\r\n\r\n"))io.Copy(os.Stdout, conn)
In this shortest code, HTTP requests were written directly to the string.In the implementation of the client rewritten in this article, requests and response textbooks are configured using standard libraries, just like server implementation.
Compatible with KEEP-ALIVE of http/1.1
In the previous code, which is simply implemented with http/1.0, the communication will be expired every time a set of communication is over.
In http/1.1, Keep-Alive has joined the standard.By using Keep-Alive, instead of cutting for each message like http/1.0, we maintain the TCP connection session for a while and use it.In TCP, it takes 1.5 RTT (round trip time: 1RTT for one round trip) to connect the session.Cutting also takes 1.5 RTT time.The time of 1 RTT varies depending on the physical distance and line speed, but if there is a lot of RTT, it will directly affect the communication speed.If there is 1.5 + 1.5 = 3 RTT overhead per transmission (1 RTT for transmission and confirmation), the execution speed is simply 1/4.Using keep-alive can help you eliminate this overhead and prevent the speed drop.
KEEP-ALIVE compatible HTTP server
長いコードなので前回のコードと重複しているところは省きます。Accept()
を呼び出した後、go
に渡している関数の内部だけを紹介します。import
には"io"
と"time"
を追加してください。
go func() { fmt.Printf("Accept %v\n", conn.RemoteAddr()) // Accept後のソケットで何度も応答を返すためにループ for { // タイムアウトを設定 conn.SetReadDeadline(time.Now().Add(5 * time.Second)) // リクエストを読み込む request, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { // タイムアウトもしくはソケットクローズ時は終了 // それ以外はエラーにする neterr, ok := err.(net.Error) // ダウンキャスト if ok && neterr.Timeout() { fmt.Println("Timeout") break } else if err == io.EOF { break } panic(err) } // リクエストを表示 dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) content := "Hello World\n" // レスポンスを書き込む // HTTP/1.1かつ、ContentLengthの設定が必要 response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, ContentLength: int64(len(content)), Body: ioutil.NopCloser( strings.NewReader(content)), } response.Write(conn) } conn.Close()}()
このコードで重要なのは、Accept()
を受信した後にfor
ループがある点です。 これにより、コネクションが張られた後に何度もリクエストを受けられるようにしています。
タイムアウトの設定も重要です。これを設定しておくと、通信がしばらくないとタイムアウトのエラーでRead()
の呼び出しを終了します。 設定しなければ相手からレスポンスがあるまでずっとブロックし続けます。 ここでは現在時刻プラス5秒を設定しています。
タイムアウトは、標準のerror
インタフェースの上位互換であるnet.Error
インタフェースの構造体から取得できます。net.Conn
をbufio.Reader
でラップして、それをhttp.ReadRequest()
関数に渡しています。 タイムアウト時のエラーはnet.Conn
が生成しますが、それ以外のio.Reader
は最初に発生したエラーをそのまま伝搬します。 そのため、error
からダウンキャストを行うことでタイムアウトかどうかを判断できます。
それ以降はほぼ一緒ですが、唯一異なるのが最後のhttp.Response
の初期化処理です。まず、HTTPのバージョンを1.1になるように設定しています。 送信するデータのバイト長が書き込まれている点もポイントです。 Go言語のResponse.Write()
は、HTTP/1.1より前もしくは長さが分からない場合はConnection: close
ヘッダーを付与してしまいます。 複数のレスポンスを取り扱うには、明確にそれぞれのレスポンスが区切れる必要があります。
KEEP-ALIVE compatible HTTP client
The client side will also support Keep-Alive.
package mainimport ( "bufio" "fmt" "net" "net/http" "net/http/httputil" "strings")func main() { sendMessages := []string{ "ASCII", "PROGRAMMING", "PLUS", } current := 0 var conn net.Conn = nil // リトライ用にループで全体を囲う for { var err error // まだコネクションを張ってない / エラーでリトライ時はDialから行う if conn == nil { conn, err = net.Dial("tcp", "localhost:8888") if err != nil { panic(err) } fmt.Printf("Access: %d\n", current) } // POSTで文字列を送るリクエストを作成 request, err := http.NewRequest( "POST", "http://localhost:8888", strings.NewReader(sendMessages[current])) if err != nil { panic(err) } err = request.Write(conn) if err != nil { panic(err) } // サーバから読み込む。タイムアウトはここでエラーになるのでリトライ response, err := http.ReadResponse( bufio.NewReader(conn), request) if err != nil { fmt.Println("Retry") conn = nil continue } // 結果を表示 dump, err := httputil.DumpResponse(response, true) if err != nil { panic(err) } fmt.Println(string(dump)) // 全部送信完了していれば終了 current++ if current == len(sendMessages) { break } }}
In the client, it is a code in which the send message is put in an array in advance and the transmission is over.Still, it became much more complicated than the first code.
サーバ同様、一度通信を開始したソケットはなるべく再利用します。 サーバ側と異なるのは、通信の起点はソケットなので、セッションが切れた場合の再接続はクライアント側にあるという点です。 切れた場合はnet.Conn
型の変数を一度クリアして再試行するようになっています。
Summary and next notice
This time, I found the basics of communication with TCP socket through HTTP.Most of the introduction of socket communication will be explained using an echo server that returns the sent content as it is.However, in this article, I implemented a communication with HTTP, which is more familiar to many readers.
Go言語では、ソケットもまた、第2回の記事から何度となく登場しているio.Writer
です。 さらにHTTPプロトコルのテキストをio.Writer
に直接読み書きする機能(Response.Write()
、Request.Write()
)が提供されています。 これらの機能を使うことで、素朴なHTTP/1.0からKeep-Aliveに対応したHTTP/1.1まで、HTTPの歴史の変化を追いかけてみました。
基本的にHTTPの歴史は、「高速化」と「セキュリティ強化」という2つのベクトルに沿って進んできました。 そんなHTTPの機能をプロトコルレベルで見ると、ソケット通信の実践的な手法がいろいろと学べます。 自分で通信処理をソケットから作るような場合も、HTTPの実装を参考にすることで、どこでどのようにループを行い、タイムアウトをどのように扱い、どのような機能セットを用意すればいいか想像しやすくなるはずです。 実際、プロトコルの読み書きをする関数やメソッドを作ってしまったら、今回紹介したコードのResponse.Write()
、Request.Write()
呼び出し部分だけが入れ替わって基本構成はほとんど変わらないコードになると思います。
We will continue to introduce TCP sockets with HTTP as the theme next time.