[備忘録]SwiftでWebSocketを試してみる



SwiftでWebSocketを試してみた。
サーバーは楽なのでSinatraを使う。。。


・Sinatra側WebSocketサーバー
(1) sinatraでwebsocket使えるようgemをインストール

```
sudo gem install sinatra-websocket

```

(2) test_app1.rb

```
# coding: utf-8                                                                                                                
require 'sinatra'
require 'sinatra/reloader'
require 'sinatra-websocket'
require 'json'

# 接続ポート                                                                                                                   
set :port, 1234
set :server, 'thin' #, group: :development                                                                                     
set :sockets, []

# 標準出力ログ出力                                                                                                             
$stdout.sync = true

get '/' do
  erb :fuga
end

get '/pingpong' do
  if request.websocket? then
    puts "request received...:[#{request.inspect}]"
    request.websocket do |ws|
      ws.onopen do
		#puts "ws on open..:[#{ws.inspect}]"
		settings.sockets << ws
      end
      ws.onmessage do |msg|
		 #puts "ws on receive msg.."
		 settings.sockets.each do |s|
          s.send("gkgk pong:#{msg}")
	     end
      end
      ws.onclose do
	     settings.sockets.delete(ws)
      end
    end
  end
end

```

/pingpongというURLでWebSocketのリクエストを受ける
クライアント側がメッセージを投げたら、"gkgk pong:#{投げたメッセージ}"を返却する感じ。


(3) views/fuga.erb

```
<!DOCTYPE HTML>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>hoge</title>

</head>
   <body>
    <form id="form">
      <input type="text" id="send_msg">
      <input type="submit">
    </form>
    <ul id="msgs"></ul>
<script type="text/javascript">
  (function (){
  window.onload = function(){
  var msgbox = document.getElementById("msgs");
  var form = document.getElementById("form");
  var send_msg = document.getElementById("send_msg");

  var tttt = 'ws://' + window.location.host + "/pingpong";
  console.log(tttt);

  var ws = new WebSocket('ws://' + window.location.host + "/pingpong");

  ws.onopen = function() {
  console.log("connection opened");
  }
  ws.onclose = function() {
  console.log("connection closed");
  }
  ws.onmessage = function(m) {
  var li = document.createElement("li");
  li.textContent = m.data;
  msgbox.insertBefore(li, msgbox.firstChild);
  }

  send_msg.onclick = function(){
  send_msg.value = "";
  }

  form.onsubmit = function(){
  ws.send(send_msg.value);
  send_msg.value = "";
  return false;
  }
  }
  })();
</script>

```

ブラウザでも確認出来るよーに、インデックスページを用意。。。

(4) サーバー起動

```
ruby ./test_app1.rb 
```

```
== Sinatra (v2.0.0) has taken the stage on 1234 for development with backup from Thin
Thin web server (v1.7.0 codename Dunder Mifflin)
Maximum connections set to 1024
Listening on 0.0.0.0:1234, CTRL+C to stop

```

ポート：１２３４で起動する。


・iOS側WebSocketクライアント

画面UIは以下のよーなイメージ。
![UIimage.png](https://qiita-image-store.s3.amazonaws.com/0/38889/03af1344-1545-301b-ee57-1a06fc83d3d8.png "UIimage.png")

イベントのログを表示するTableViewを画面上部、
真ん中に、送信するメッセージを入力するテキストボックス、
その下に、サーバーとの接続ボタンを配置した。


(1) シングルビューのSwiftのプロジェクトを作成し、cocoapodsで、Swift用のWebSocketのライブラリをインストール

```
platform :ios, '9.0'
use_frameworks!

target 'WebSockTest1' do
  pod 'Reachability'
  pod 'XCGLogger'
  pod 'SwiftWebSocket'
end

```

プロジェクト名は、「WebSockTest1」で作成している。
使用したライブラリは、「SwiftWebSocket」。

(2) ViewControllerのソースを以下のように。。。

```
import UIKit

import SwiftWebSocket

class ViewController: UIViewController,
    UITableViewDataSource,
    UITableViewDelegate,
    UITextFieldDelegate {

    
    static let HOGE_URL = "ws://localhost:1234/pingpong"
    
    
    @IBOutlet weak var tblChat: UITableView!
    
    @IBOutlet weak var opeContainer: UIView!
    
    @IBOutlet weak var txtMessage: UITextField!
    
    @IBOutlet weak var btnConnect: UIButton!
    
    @IBOutlet weak var opeContainerHeight: NSLayoutConstraint!

    var ws: WebSocket!
    var flgWsInit = false

    var eventLogs = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        setupWs()
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func tapBtnConnect(_ sender: Any) {
        
        if ws == nil || ws.readyState != .open {
            if ws == nil {
                setupWs()
            }
            ws.open(ViewController.HOGE_URL)
            btnConnect.isEnabled = false
        }
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        let msg = textField.text!
        if msg == "" {
            return true
        }
        if ws.readyState == .open {
            ws.send(msg)
            self.addWsEvent(ev: "send: \(msg)")
        }
        textField.text = ""
        return true
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0.0
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80.0
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        var cell: EventCell?
        
        cell = tableView.dequeueReusableCell(withIdentifier: "CELL_EV",
                                             for: indexPath) as? EventCell
        
        if cell == nil {
            
            cell = EventCell.instance()
        }

        cell!.txtEvent.text = eventLogs[indexPath.item]
        
        return cell!
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return eventLogs.count
    }
    
    
    private func setupUI() {

        txtMessage.text = ""
        txtMessage.clearButtonMode = .always
        txtMessage.returnKeyType = .go
        txtMessage.autocapitalizationType = .none
        txtMessage.spellCheckingType = .no
        txtMessage.delegate = self
        if txtMessage.canBecomeFirstResponder {
            txtMessage.becomeFirstResponder()
        }

        tblChat.layer.borderWidth = 1.0
        tblChat.layer.borderColor = UIColor.lightGray.cgColor
        tblChat.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
        tblChat.register(
            UINib(nibName: "EventCell", bundle: nil),
            forCellReuseIdentifier: "CELL_EV")
        tblChat.dataSource = self
        tblChat.delegate = self
    
    }
    

    func addWsEvent(ev: String) {

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[unowned self]  in

            self.tblChat.beginUpdates()
            self.eventLogs.insert(ev, at: 0)
            let indexPath = IndexPath(row: 0, section: 0)
            self.tblChat.insertRows(at: [indexPath], with: .automatic)
            self.tblChat.endUpdates()
            
            for idx in self.tblChat.indexPathsForVisibleRows! {
                if idx.item != 0 {
                    self.tblChat.cellForRow(at: idx)?.setSelected(false, animated: false)
                }
            }
            self.tblChat.cellForRow(at: indexPath)?.setSelected(true, animated: false)
            
            if self.txtMessage.canBecomeFirstResponder {
                self.txtMessage.becomeFirstResponder()
            }
        }
    }

    
    private func setStateBtnConn(isEnable: Bool) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {[unowned self]  in
            self.btnConnect.isEnabled = isEnable
        }
    }
    
    
    private func setupWs() {
    
        ws = WebSocket()
//        ws = WebSocket(ViewController.HOGE_URL)
        if !flgWsInit {
            ws.event.open = {[unowned self] in
                log.debug("opened:\(ViewController.HOGE_URL)")
                self.addWsEvent(ev: "opened:\(ViewController.HOGE_URL)")
                self.setStateBtnConn(isEnable: false)
            }
            ws.event.close = {[unowned self] code, reason, clean in
                log.debug("close")
                self.addWsEvent(ev: "close.")
                self.setStateBtnConn(isEnable: true)
            }
            ws.event.error = {[unowned self] error in
                log.debug("error \(error)")
                self.addWsEvent(ev: "error \(error)")
            }
            ws.event.message = {[unowned self] message in
                if let text = message as? String {
                    log.debug("recv: \(text)")
                    self.addWsEvent(ev: "recv: \(text)")
                    if self.ws.readyState != .open {
                        self.setStateBtnConn(isEnable: false)
                    }
                }
            }
            flgWsInit = true
        }
    }
    
}

```


iOSアプリ側の仕様は、

・ConnectボタンでWebSocketサーバーに接続する。
・テキストボックスにメッセージを入力し、エンターキーを入力すると、サーバーにメッセージを送信する。
・サーバーからのレスポンスは、UITableViewのセルを追加して表示する。
（UIスレッドじゃないはずなので、表示はUIスレッドに遅延で更新するようにした。（DispatchQueue.main.asyncAfter））

という感じ。

ローカルでテストするので、Info.plistにセキュアでない通信を許可するよう(ATS)設定しておく。

![plist.png](https://qiita-image-store.s3.amazonaws.com/0/38889/dc30c6dd-2486-ac1f-e094-93eb4eb7509f.png "plist.png")




・sinatraを起動した状態で、エミュレーターで実行してみる。

(1)アプリ起動
![UIimage.png](https://qiita-image-store.s3.amazonaws.com/0/38889/24ceeb86-f1ac-baa2-5ed2-a2a16fcfb5df.png "UIimage.png")


(2)Connectボタンをタップ
![UIimage_tap_conn.png](https://qiita-image-store.s3.amazonaws.com/0/38889/26b25150-d1fb-19b5-962f-2096499e1d1f.png "UIimage_tap_conn.png")

接続したログが、テーブルビューに追加される。

(3)テキストボックスに「HOGE」を入力し、エンターキーを押す。
![UIimage_send_hoge.png](https://qiita-image-store.s3.amazonaws.com/0/38889/61ce1c28-a6f5-74ff-28e5-da5b560027fc.png "UIimage_send_hoge.png")

「HOGE」を送信したログが追加され、続けて、
「gkgk pong:HOGE」がサーバーから返答されてるログを表示。

(4)インデックスページを立ち上げ、エミュレーターをそのままにし、
インデックスページからメッセージを送信してみる。

![sinatra_index.png](https://qiita-image-store.s3.amazonaws.com/0/38889/06529da7-1d2f-609a-6f9b-7686cc38001c.png "sinatra_index.png")


(5)「fuga」と入力し、送信ボタンをタップ。

![sinatra_send_fuga.png](https://qiita-image-store.s3.amazonaws.com/0/38889/1f71118e-bdfe-813b-8142-5f7fc1d56741.png "sinatra_send_fuga.png")

エミュレーターの状態を見てみる。
![UIimage_receive_fuga.png](https://qiita-image-store.s3.amazonaws.com/0/38889/0564d438-18e0-f4eb-bdd8-fd6dd5660ac6.png "UIimage_receive_fuga.png")

「fuga」を受信している。

これは、
Sinatra上で接続をArrayにキャッシュしていて、いま接続中のコネクションに全てに対して、メッセージを送信しているから。


という感じで、SinatraとCocoaPodsのライブラリで簡単に試すことが出来た。。。