NestJSでWebSocket

本記事は [NestJS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nestjs) 19日目の記事です。
この記事の公開日は12月28日でクリスマスを過ぎていますが、アドカレが空いているためせっかくなので登録させていただきます。
NestJSのAdventカレンダーを盛り上げてくださったみなさん、本当にありがとうございました。
大変参考になりました。

## NestJS実装

### プロジェクト作成
```console:
nest new websocket-server
```

### パッケージインストール
```console:
$ npm i --save @nestjs/websockets @nestjs/platform-socket.io
$ npm i --save-dev @types/socket.io
```

### WebsocketGatewayを追加する
```console:
$ nest g gateway Events
CREATE /src/events.gateway.spec.ts (460 bytes)
CREATE /src/events.gateway.ts (240 bytes)
```

作成されたクラスを以下のように修正します。

```javascript:events.gateway.ts
import {
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  WsResponse,
  MessageBody
} from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway()
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: any): WsResponse<string> {
    return { event: 'message', data: data };
  }
}
```

今回はクライアントからリクエストされた内容をそのままオウム返しする処理にしてみました。

## クライアント

クライアント側は[Angular2 + Socket.ioで遊んでみる](https://qiita.com/cilly/items/833bc20784b0a7d56d03)の記事を参考にさせていただき実装します。


### プロジェクト作成

```console:
$ ng new websocket-client
```

### パッケージインストール

```console:
$ npm install socket.io-client --save
$ npm install @types/socket.io-client --save-dev
```

### サービス作成

websocket通信を行うサービスを作成します。

```console:
$ ng generate service websocket
CREATE src/websocket.service.spec.ts (348 bytes)
CREATE src/websocket.service.ts (138 bytes)
```

```javascript:websocket.service.ts
import { Injectable } from '@angular/core';

import * as io from 'socket.io-client';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class WebsocketService {
  private url     = 'http://localhost:3001';
  private socket;

  connect() {
    this.socket = io(this.url);
   }
  
  emit(emitName: string, data?) {
    this.socket.emit(emitName, data);
  }
  
  on(onName: string) {
    let observable = new Observable(observer => {
      this.socket.on(onName, (data) => {
        observer.next(data);
      });
  
      return () => { this.socket.disconnect(); };
    });
    return observable;
  }
}
```

### コンポーネント作成

最後にコンポーネントを作成します。

```console:
$ ng generate component websocket
```

```javascript:websocket/websocket.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebsocketService } from '../websocket.service';

@Component({
  selector: 'ss-test-websocket',
  templateUrl: './websocket.component.html',
  styleUrls: ['./websocket.component.scss']
})
export class WebsocketComponent implements OnInit, OnDestroy {
  connection;
  data;
  message;

  constructor(private websocketService: WebsocketService) {}

  onClick(){
    this.websocketService.emit('events', { test: 'test' });
    console.log('click start');
    console.log('click end')
  }

  ngOnInit() {
    this.websocketService.connect();
    this.connection = this.websocketService.on('message').subscribe(data => {
      console.log(data);
      this.message = data;
    })
    this.websocketService.emit('message', 'Hello!!');
  }

  ngOnDestroy() {
    this.connection.unsubscribe();
  }
}
```

```html:websocket/websocket.component.html
<p>websocket works!</p>
{{ message }}
```

```javascript:app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WebsocketComponent } from './websocket/websocket.component';

const routes: Routes = [
  { path: 'websocket', component: WebsocketComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
```

## 実行結果
|![ブラウザ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406169/d681f370-ab84-4d46-3a88-215b268f59b6.png)|
|:-:|


## クライアント側にブロードキャストする

上記の処理は、リクエストに対してレスポンスするだけの処理だったのですが、コネクション接続しているクライアント全員に対してブロードキャストメッセージを送信する方法を紹介します。

### サーバ

```javascript:events.gateway.ts
import {
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
  WsResponse,
  MessageBody
} from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway(3001)
export class EventsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: any): WsResponse<string> {
    return { event: 'response', data: data };
  }

  @SubscribeMessage('broadcast')
  broadcast(@MessageBody() data: any): void {
    this.server.emit('broadcast', 'response')
  }
}
```

server.emitでメッセージを送信しています。

### クライアント

```javascript:websocket/websocket.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { WebsocketService } from '../websocket.service';

@Component({
  selector: 'ss-test-websocket',
  templateUrl: './websocket.component.html',
  styleUrls: ['./websocket.component.scss']
})
export class WebsocketComponent implements OnInit, OnDestroy {
  connection;
  message;

  constructor(private websocketService: WebsocketService) {}

  onClick(){
    this.websocketService.emit('broadcast', 'Hello!');
  }

  ngOnInit() {
    this.websocketService.connect();
    this.connection = this.websocketService.on('broadcast').subscribe(data => {
      console.log(data);
      this.message = data;
    })
  }

  ngOnDestroy() {
    this.connection.unsubscribe();
  }
}
```

```html:websocket/websocket.component.html
<p>websocket works!</p>
<button (click)="onClick()">Click</button>
{{ message }}
```

### 結果

![websocket.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406169/5b49b76f-d469-62ac-2878-26692fd8c43a.gif)

こちらを利用すればチャットなども作成可能です。


## 参考資料

[NestJS->WEBSOCKETS->Gateways](https://docs.nestjs.com/websockets/gateways)
[公式のサンプルコード](https://github.com/nestjs/nest/tree/master/sample/02-gateways)
[Angular2 + Socket.ioで遊んでみる](https://qiita.com/cilly/items/833bc20784b0a7d56d03)


### その他
上記の内容に加えて、NestJSのwebsocketをAPIGateway+Lambdaでサーバレス実装を載せようと思っていたのですが、WebSocketGatewayの呼び出しがうまくいかず断念しました・・・
RESTはコントローラを呼び出す設定に普通にできたのですが。
今後色々試して出来たらまたこちらを更新します。
もしプロキシする方法わかる人は教えてください。