F#でソケットプログラミング入門

F#とソケットプログラミングを勉強中なので両方を使って何か作ってみたい。
定番のEchoサーバと、コンソールで使えるチャットサーバも挑戦してみた。

# Echoサーバ

https://github.com/dyoshikawa/fsharp-echo-server

## サーバ

```fsharp
open System.IO
open System.Net
open System.Net.Sockets
open System.Text

[<EntryPoint>]
let main _argv =
    let endPoint = new IPEndPoint(IPAddress.Any, 8080)
    let server = new Socket(AddressFamily.InterNetwork,
                            SocketType.Stream,
                            ProtocolType.Tcp)
    server.Bind(endPoint)
    server.Listen(10)
    
    printfn "server started (%s)" <| server.LocalEndPoint.ToString()
    
    let rec serve () =
        let client = server.Accept()
        printfn "client accepted (%s)" <| client.RemoteEndPoint.ToString()
        
        let rec read () =
            let buffer = [| for _ in 1..10 -> byte(0) |]
            let len = client.Receive(buffer)
            if len = 0 then
                printfn "接続が切れました"
                client.Close()
                ()
            else
                printfn "%s" <| Encoding.Default.GetString(buffer, 0, len)
                client.Send(buffer) |> ignore
                read ()
        read ()

        client.Close()
        
        serve ()
    
    serve () |> ignore
    
    0
```

## クライアント

```fsharp
open System.IO
open System.Net
open System.Net.Sockets
open System.Text

[<EntryPoint>]
let main _argv =
    printfn "送信する文字列を入力して下さい"
    let input = stdin.ReadLine()
    
    let socket = new Socket(AddressFamily.InterNetwork,
                            SocketType.Stream,
                            ProtocolType.Tcp)
    
    let endPoint = new IPEndPoint(IPAddress.Loopback, 8080)
    
    printfn "接続開始"
    socket.Connect(endPoint)
    
    let sendBuf = Encoding.Default.GetBytes(input)
    let sendBufLen = sendBuf.Length
    
    printfn "送信開始"
    let sendResult = socket.Send(sendBuf)
    if sendResult = -1 then
        printfn "送信失敗"
        socket.Close()
        1
    else
        printfn "送信完了"
        printfn "受信開始"
        let memory = new MemoryStream()
        let rec receive (total : int) : unit =
            if total >= sendBufLen then
                printfn "受信完了"
                printfn "接続終了"
                socket.Close()
                ()
            else
                let buffer = [| for _ in 1..10 -> byte(0) |]
                let len = socket.Receive(buffer)
                memory.Write(buffer, 0, len)
                receive (total + len)
        receive 0
        printfn "受信: %s" <| Encoding.Default.GetString(memory.GetBuffer())
        socket.Close()
        0

```

## 動作確認

```
dotnet run --project Server
```

でサーバを起動。

```
dotnet run --project Client
```

でクライアント起動。
何か文字列を入力して改行すると送信。
その後のサーバからの受信内容を表示する。

# チャットサーバ

https://github.com/dyoshikawa/fsharp-tcp-chat

## サーバ

```fsharp
open System.IO
open System.Net
open System.Net.Sockets
open System.Text

[<EntryPoint>]
let main _argv =
    printfn "接続先を入力して下さい"
    let inputIp = stdin.ReadLine()

    let socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)

    let ip =
        match inputIp with
        | "" -> IPAddress.Loopback
        | _ -> IPAddress.Parse(inputIp)

    let endPoint = IPEndPoint(ip, 8080)

    printfn "接続開始"
    socket.Connect(endPoint)
    printfn "接続成功"
    printfn "チャット開始"

    async {
        let rec receive(): unit =
            let buf =
                [| for _ in 1 .. 10 -> byte (0) |]

            let len = socket.Receive(buf)
            printf "%s" <| Encoding.Default.GetString(buf)
            receive()
        receive()
    }
    |> Async.Start

    let rec sendText(): int =
        let inputText = stdin.ReadLine()

        let sendBuf =
            Encoding.Default.GetBytes(sprintf "%s: %s\r\n" (socket.LocalEndPoint.ToString()) inputText)

        let sendResult = socket.Send(sendBuf)
        if sendResult = -1 then
            printfn "送信失敗"
            socket.Close()
            1
        else
            sendText()
    sendText()

```

複数のクライアントからの受信監視をマルチスレッドで実現している。
すべてのクライアント情報を突っ込んだ配列をスレッド間で共有したかったため、スレッドセーフなBlockingCollectionを使用。

## クライアント

```fsharp
open System.Collections.Concurrent
open System.Net
open System.Net.Sockets
open System.Text

let sendBufToClients (clients: BlockingCollection<Socket>) (own: Socket) (buf: byte []): unit =
    for c in clients do
        if c.Equals(own) then ()
        else c.Send(buf) |> ignore

[<EntryPoint>]
let main _argv =
    let endPoint = IPEndPoint(IPAddress.Any, 8080)
    let socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    socket.Bind(endPoint)
    socket.Listen(10)

    printfn "チャットサーバ開始 (%s)" <| socket.LocalEndPoint.ToString()

    let clients = new BlockingCollection<Socket>()

    let rec launch() =
        let client = socket.Accept()
        clients.Add(client)

        let newClientMsg =
            sprintf "入室 %s\r\n"
            <| client.RemoteEndPoint.ToString()
        printfn "%s" newClientMsg
        sendBufToClients clients client <| Encoding.Default.GetBytes newClientMsg

        async {
            let rec read() =
                let buffer =
                    [| for _ in 1 .. 10 -> byte (0) |]

                let len = client.Receive(buffer)
                if len = 0 then
                    let exitClientMsg =
                        sprintf "退室 %s\r\n"
                        <| client.RemoteEndPoint.ToString()
                    printfn "%s" exitClientMsg
                    sendBufToClients clients client <| Encoding.Default.GetBytes exitClientMsg
                    client.Close()
                    ()
                else
                    printf "%s" <| Encoding.Default.GetString(buffer, 0, len)
                    sendBufToClients clients client buffer
                    read()
            read()
        }
        |> Async.Start

        launch()

    launch() |> ignore

    0

```

## 動作確認

```
dotnet run --project Server
```

でサーバを起動。

```
dotnet run --project Client
```

でクライアントを起動。
サーバに接続し、発言が入力待ちになるので、文字を入力して改行でメッセージを送信する。
サーバ側でメッセージを受け取り、他のクライアントへ送信する。

# 参考

[TCP/IPソケットプログラミング C言語編](https://www.amazon.co.jp/TCP-IP%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-C%E8%A8%80%E8%AA%9E%E7%B7%A8-Michael-Donahoo/dp/4274065197/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&keywords=TCP%2FIP+%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0&qid=1575217749&sr=8-1)
https://webbibouroku.com/Blog/Article/socket-server
http://karoten512.hatenablog.com/entry/2018/03/21/234156
https://ameblo.jp/tasoh/entry-10274850055.html
https://smdn.jp/programming/netfx/tips/echo_server/