LoginSignup
2

More than 3 years have passed since last update.

posted at

updated at

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

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

Echoサーバ

サーバ

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

クライアント

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

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

チャットサーバ

サーバ

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を使用。

クライアント

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://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/

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2