Pythonで、CUIのチャットもどきを作ってみる

学習履歴

■はじめに

チェットアプリを作成しようと思い、一カ月前に [pythonでsocket通信を勉強しよう] にて、socket 通信を学んだ。

だいぶ時間が経ってしまったが、CUI 上で簡単なメッセージをやり取りできるチャットもどきを
作成したので、備忘を残しておく。

■CUIのチャットもどき

socket_server.py 及び socket_client.py ファイルを同一ディレクトリ内に作成する。

socket_server.py
import socket
import threading


class SocketServer():
    def __init__(self):
        self.host = socket.gethostname()
        self.port = 50007
        self.clients = []

    def socket_server_up(self):
        # ソケットサーバ作成(IPv4, TCP)
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 接続待ちするサーバのホスト名とポート番号を指定
        sock.bind((self.host, self.port))
        # 5 ユーザまで接続を許可
        sock.listen(5)
        while True:
            try:
                # 接続要求を受信
                conn, addr = sock.accept()
            except KeyboardInterrupt:
                break
            # アドレス確認
            print("[接続]{}".format(addr))
            # クライアントを追加
            self.clients.append((conn, addr))
            # スレッド作成
            thread = threading.Thread(target=self.handler, args=(conn, addr), daemon=True)
            # スレッドスタート
            thread.start()

    def close_connection(self, conn, addr):
        print('[切断]{}'.format(addr))
        # 通信を遮断する
        conn.close()
        # クライアントを除外する
        self.clients.remove((conn, addr))

    def handler(self, conn, addr):
        while True:
            try:
                # クライアントから送信されたメッセージを 1024 バイトずつ受信
                data = conn.recv(1024)
            except ConnectionResetError:
                # クライアント側でソケットを強制終了(Ctrl + c)すると
                # ソケットサーバが処理落ちするので、コネクションを切断する
                self.close_connection(conn, addr)
                break

            if not data:
                # データが無い場合、接続を切る
                self.close_connection(conn, addr)
                break
            else:
                print('data : {}, addr&port: {}'.format(data, addr))
                for client in self.clients:
                    try:
                        client[0].sendto(data, client[1])
                    except ConnectionResetError:
                        break


if __name__ == "__main__":
    ss = SocketServer()
    ss.socket_server_up()
socket_client.py
import socket
import threading
import socket_server
import random


class SocketClient():
    def __init__(self):
        self.host = socket.gethostname()
        self.port = 50007
        self.number = random.randint(1, 10)


    def socket_client_up(self):
        print('{}さん、こんにちは。チャットを開始します。'.format(self.number))
        # クライアントソケット作成(IPv4, TCP)
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
            try:
                # サーバソケットへ接続しに行く(サーバのホスト名, ポート番号)
                sock.connect((self.host, self.port))
                # スレッド作成
                thread = threading.Thread(target=self.handler, args=(sock,), daemon=True)
                # スレッドスタート
                thread.start()
                # クライアントからメッセージを送る
                self.send_message(sock)
            except ConnectionRefusedError:
                # 接続先のソケットサーバが立ち上がっていない場合、
                # 接続拒否になることが多い
                print('ソケットサーバに接続を拒否されました。')
                print('ソケットサーバを立ち上げます。')
                print('Starting....')
                ss = socket_server.SocketServer()
                ss.socket_server_up()

    def send_message(self, sock):
        while True:
            try:
                # ユーザのキーボード入力を受け取る
                msg = "[{}]".format(self.number) + input()
            except KeyboardInterrupt:
                msg = '{} さんが退出しました。'.format(self.number)
                # メッセージ送信
                sock.send(msg.encode('utf-8'))
                break
            if msg == '[{}]exit'.format(self.number):
                msg = '{} さんが退出しました。'.format(self.number)
                # メッセージ送信
                sock.send(msg.encode('utf-8'))
                break
            elif msg:
                try:
                    # メッセージ送信
                    sock.send(msg.encode('utf-8'))
                except ConnectionRefusedError:
                    # 接続先のソケットサーバが立ち上がっていない場合、
                    # 接続拒否になることが多い
                    break
                except ConnectionResetError:
                    break

    def handler(self, sock):
        while True:
            try:
                # クライアントから送信されたメッセージを 1024 バイトずつ受信
                data = sock.recv(1024)
                print("  {}".format(data.decode("utf-8")))
            except ConnectionRefusedError:
                # 接続先のソケットサーバが立ち上がっていない場合、
                # 接続拒否になることが多い
                break
            except ConnectionResetError:
                break


if __name__ == "__main__":
    sc = SocketClient()
    sc.socket_client_up()

■実行方法

1.socket_server.py 及び socket_client.py ファイルを格納したフォルダにコマンドラインでアクセスする。
2. socket_server.py を起動 ※ソケットサーバを先に立ち上げる
3. socket_client.py を起動
4. 適当にメッセージを入力する

img.png

左側のコマンドラインがソケットサーバで、右側の 2 つのコマンドラインがクライアントだ。

今回、クライアントの区別がつくように、random 関数で適当な数字を作成し、ユーザ名の代わりにした。

メッセージのやり取りは問題なくできており、チャットができているようにみえる。

ただし、このチャットもどきにはバグがある。

■バグ

このチャットもどきは、クライアント側で exit と打つか、Ctrl + c で強制終了すると抜けることができる。

その時に、「○○さんが退出しました。」というメッセージを出すように実装したが、このメッセージが、
出るときと出ないときがある。

<メッセージが出ないとき>
img.png

<メッセージが出るとき>
img.png

原因は今の所、確認中だ。
並列処理にスレッドを使用しているが、そこの理解が足りないのかもしれない。

■まとめ

今回は、CUI 上で動くチャットもどきを作成したが、次は、Django で本格的なチャットを作ってみたい。
ただ、その前にスレッドの勉強をしようと思う。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.