Help us understand the problem. What is going on with this article?

Pythonでソケットを使った単一のソケットサーバー VS 複数のソケットクライアントの双方向チャット。

More than 1 year has passed since last update.

Pythonによる複数クライアントVS単一のソケットサーバー

前回、PHPによる複数クライアントが同一のソケットサーバーにアクセスし複数のクライアントとコンソール上で、
チャットをやりとりするソケット通信をためしたが、今回はPythonにてそれを実現する。

(1)ソケットクライアントでHTTPリクエスト

まず、Pythonでソケットクライアントを作成し、
一般的なWebサイトへGETリクエストをなげる。

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 接続先
host = "www.yahoo.co.jp"
port = 80
sock.connect((host, port));

req = "GET / HTTP/1.1" + "\r\n\r\n";
sock.send(req.encode("UTF-8"));

read_size = 128
res = "".encode("UTF-8")
while True:
    try:
        sock.settimeout(3)
        t = sock.recv(read_size);
        res += t
        if (len(t) == 0) :
            print(res.decode())
            print("====サーバーからの読み取り終了====")
            break;
        #end
    except Exception as e:
        print(e);

# 受信結果
...
...
...
Copyright (C) 2018 Yahoo Japan Corporation. All Rights Reserved.
</address>
</div><!--/#footer-->

</div><!--/#wrapper-->

</body>
</html>

====サーバーからの読み取り終了====

上記の通り、GETリクエストの結果を取得する。(SSL対応が多少手間がかかるため)
この場合sslを使わず80portでリクエストしている。

Pythonの場合

        t = sock.recv(read_size);

通常Pythonによるサーバー側からの返却値の取得の再に、
上記の[recv]メソッド時点において完全にスレッドをブロックしてしまう。
そのためここでは事前に以下のように対処する。

# タイムアウトの例外をキャッチさせる
sock.settimeout(3);

上記のようにタイムアウトを指定することでループ内での処理を可能とする

(2)Pythonでクライアントを受信するサーバーをつくる

では、次にソケットクライアントを待ち受けるソケットサーバーを作成する。
まず単一のクライアントのみを受け付ける。
事前に、自由入力で繰り返しサーバーに文字列を送信できる
ソケットクライアントを作成します

client.py
import socket

try:

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    host = "127.0.0.1"
    port = 55580

    s.connect((host, port))
except Exception as e:
    print(e)

print(s)
while (True):
    try:
        # サーバ側へ送信するメッセージ
        your_input = input(">>> ")
        if len(your_input) > 0:
            s.send(your_input.encode("UTF-8"))
            # サーバーからのレスポンス
            while(True):
                s.settimeout(3)
                res = s.recv(4096)
                print(res.decode())
                if (len(res) == 0):
                    # 受信内容を出力
                    break

        else:
            continue
    except Exception as e:
        print(e)
        continue

上記は、コンソールから一行分の任意の文字列を入力し
それをソケットサーバ側に送信し、そのままサーバーからの戻りの値を
受信する。しかしサーバー側からなにも受信できないままだと

                res = s.recv(4096)

[recv]メソッドの呼び出し時点で永遠にスレッドをブロックしてしまうため

                s.settimeout(3)

直前にソケットのタイムアウトを設定しています
タイムアウトが発生した場合、そのまま例外がスローされるので
except句でキャッチし再度入力のフローへと移動させます

次にソケットサーバー側です

unit_server.py
import socket


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
host = "127.0.0.1"
port = 55580
s.bind((host, port))
s.listen(10)


clients = []

try:
    s.settimeout(3)
    connection, address = s.accept()
    clients.append((connection, address))
    while(True):
        try:
            connection.settimeout(3)
            from_client = connection.recv(4096).decode()
            print("クライアントから受信したメッセージ=>{}".format(from_client))
            to_client = "あなたは[{}]というメッセージを送信しましたね?".format(from_client)
            connection.send(to_client.encode("UTF-8"))
        except Exception as e:
            print(e)
            continue
except Exception as e:
    print(clients)
    print(e)
    connection.close()
    s.close()

上記のコードが単一(1クライアントのみ)のみのソケットサーバです
動作させた際の動きは,

クライアント側のコンソール表示内容
python client_py.py
<socket.socket fd=456, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 59437), raddr=('127.0.0.1', 55580)>
>>>
>>>
>>>
>>>
>>>
>>> 僕の名前はやんぼー
あなたは[僕の名前はやんぼー]というメッセージを送信しましたね?
timed out
>>> きーみと僕とでヤンマーだ!
あなたは[きーみと僕とでヤンマーだ!]というメッセージを送信しましたね?
timed out
>>> 小さなものか大きなものまでなんちゃら~
あなたは[小さなものか大きなものまでなんちゃら~]というメッセージを送信しましたね?
timed out
>>>
サーバー側の表示内容
python unit_server.py
timed out
timed out
クライアントから受信したメッセージ=>僕の名前はやんぼー
timed out
timed out
timed out
timed out
timed out
timed out
クライアントから受信したメッセージ=>きーみと僕とでヤンマーだ!
timed out
timed out
timed out
timed out
クライアントから受信したメッセージ=>小さなものか大きなものまでなんちゃら~

うまい具合にクライアントとサーバー側の無限のやり取りが成功しています
しかし、この場合単一のクライアントのみを受け付けるよう実装しているため
2個目のクライアントは接続できません

これでは全くソケットを使ったという楽しみが無いため、
次に、複数のクライアントの受付ができるように改良します

(3)Pythonで複数のクライアントを受付可能なサーバーをつくる

さて、PHPはマルチスレッドを標準では利用できないため
stream_select()を用いて複数のクライアントの監視処理を実装しましたが、
Pythonではマルチスレッドを利用できるため以下のようにスレッドを利用しています

multi_server.py
import socket;
import select;
import threading

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 接続待ちするサーバのホスト名とポート番号を指定
host = "127.0.0.1";
port = 55580
argument = (host, port)
sock.bind(argument)
# 5 ユーザまで接続を許可
sock.listen(5)
clients = []

# 接続済みクライアントは読み込みおよび書き込みを繰り返す
def loop_handler(connection, address):
    while True:
        try:
            #クライアント側から受信する
            res = connection.recv(4096)
            for value in clients:
                if value[1][0] == address[0] and value[1][1] == address[1] :
                    print("クライアント{}:{}から{}というメッセージを受信完了".format(value[1][0], value[1][1], res))
                else:
                    value[0].send("クライアント{}:{}から{}を受信".format(value[1][0], value[1][1], res.decode()).encode("UTF-8"))
                    pass
        except Exception as e:
            print(e);
            break;


while True:
    try:
        # 接続要求を受信
        conn, addr = sock.accept()

    except KeyboardInterrupt:
        sock.close()
        exit()
        break
    # アドレス確認
    print("[アクセス元アドレス]=>{}".format(addr[0]))
    print("[アクセス元ポート]=>{}".format(addr[1]))
    print("\r\n");
    # 待受中にアクセスしてきたクライアントを追加
    clients.append((conn, addr))
    # スレッド作成
    thread = threading.Thread(target=loop_handler, args=(conn, addr), daemon=True)
    # スレッドスタート
    thread.start()

では冒頭に記載した[client.py]と上記の[multi_server.py]をコンソールで起動して
チャットを実行すると・・・

サーバー側ログ
python multi_server.py
[アクセス元アドレス]=>127.0.0.1
[アクセス元ポート]=>59495


[アクセス元アドレス]=>127.0.0.1
[アクセス元ポート]=>59496


クライアント127.0.0.1:59495からb'Python\xe3\x81\xa7\xe3\x83\x9e\xe3\x83\xab\xe3\x83\x81\xe3\x82\xb9\xe3\x83\xac\xe3\x83\x83\xe3\x83\x89\xe6\xa5\xbd\xe3\x81\x97\xe3\x81\x84\xe3\x81\xaa!'というメッセージを受信完了
クライアント127.0.0.1:59496からb'Python\xe3\x81\xa7\xe3\x82\xbd\xe3\x82\xb1\xe3\x83\x83\xe3\x83\x88\xe3\x82\xaf\xe3\x83\xa9\xe3\x82\xa4\xe3\x82\xa2\xe3\x83\xb3\xe3\x83\x88\xe6\xa5\xbd\xe3\x81\x97\xe3\x81\x84\xe3\x81\xaa!'というメッセージを 受信完了
クライアント127.0.0.1:59495からb'Python\xe3\x81\xa7\xe3\x82\xbd\xe3\x82\xb1\xe3\x83\x83\xe3\x83\x88\xe3\x82\xb5\xe3\x83\xbc\xe3\x83\x90\xe3\x83\xbc\xe6\xa5\xbd\xe3\x81\x97\xe3\x81\x84\xe3\x81\xaa!'というメッセージを受信完了
クライアント(1)
python client_py.py
<socket.socket fd=144, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 59495), raddr=('127.0.0.1', 55580)>
>>> Pythonでマルチスレッド楽しいな!
timed out
>>> Pythonでソケットサーバー楽しいな!
クライアント127.0.0.1:59495からPythonでソケットクライアント楽しいな!を受信
timed out
>>>

クライアント(2)
python client_py.py
<socket.socket fd=448, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 59496), raddr=('127.0.0.1', 55580)>
>>> Pythonでソケットクライアント楽しいな!
クライアント127.0.0.1:59496からPythonでマルチスレッド楽しいな!を受信
timed out
>>>

以上、上記のように2つのクライアントが送信したメッセージを
お互いソケットサーバを介して、互いのクライアントへとメッセージの交換が
実行できています

しかし、残念ながら前述のクライアントの実装方法では
自分以外のクライアントのメッセージを受け取るためには必ず自分も
なにかしらの値の送信を行わなければ受信ができません。つまり
サーバーになにかしらリクエストを送り、その直後にサーバからの返答を
受信しているのです。これでは不便きわまりないです

そこでクライアント側もメッセージの送信と受信のスレッドを
分離してみます

(4)クライアント側の受信を別スレッドに分離

multi_client.py
import socket
import threading
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 接続先
host = "127.0.0.1"
port = 55580
sock.connect((host, port));

def Handler(sock):
    while True:
        try:
            read = sock.recv(4096); #(3)
            print("読み込んだバイト数:({})".format(len(read)));
            print("<"+read.decode()+">");
            if (len(read) < 4096) :
                continue
            #end
        except Exception as e:
            continue


while (True):
    your_input = input(">>>"); #(1)
    print(sock.send(your_input.encode("UTF-8"))); #(2)
    thread = threading.Thread(target = Handler, args= (sock,), daemon= True)
    thread.start();


上記のmulti_client.pyというクライアントと
multi_server.pyというサーバーでソケット通信を実行すると?***

クライアント(1)
python multi_client.py
>>>先週新しいクレヨンしんちゃんの役者さんの声を一瞬だけ聞きました
93
>>>読み込んだバイト数:(162)
<クライアント127.0.0.1:59547からもうずいぶんクレヨンしんちゃんは見ていませんが、全く違和感を感じませんでしたを受信>
break
そういえば、藤原啓治はもう復帰しましたか?
61
>>>読み込んだバイト数:(97)
<クライアント127.0.0.1:59547から野原ひろし役はいつ復帰するんです?を受信>
break

クライアント(2)
python multi_client.py
>>>もうずいぶんクレヨンしんちゃんは見ていませんが、全く違和感を感じませんでした
114
読み込んだバイト数:(141)
<クライアント127.0.0.1:59548から先週新しいクレヨンしんちゃんの役者さんの声を一瞬だけ聞きましたを受信>
break
>>>読み込んだバイト数:(109)
<クライアント127.0.0.1:59548からそういえば、藤原啓治はもう復帰しましたか?を受信>
break
野原ひろし役はいつ復帰するんです?
49
>>>

上記の実行例だとわかりにくいですが、
サーバーへの送信と受信をわけているためクライアントの入力待ちの間でも別のだれかが
入力を行うとその他のクライアントのコンソール上に強制的に出力されます。
これでようやくまともなソケットチャットが実現できました。

yayoshM
とある福岡の地場企業
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away