diff --git a/README.md b/README.md index beb5f3b..d8481e9 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,39 @@ # Python Socket Programming +

drawing

## About -A sample socket programming in python using low level networking interface module [socket](https://docs.python.org/3.3/library/socket.html). + +A sample socket programming in python using low level networking interface module [socket](https://docs.python.org/3.3/library/socket.html). + +Inside 'cam_streaming' directory one could the variant of client and server script which reads the cam data and on client side and send it to server. Then server could show up to two stream for received cam images. Among the two stream, for one stream latency could be added. Check config file inside cam_streaming directory to set the configuration. + +## Instructions + +* Create conda environment with required dependencies by running: + + ```bash + conda env create --name socket_prog --file conda_env_export.yml + ``` + +* Run server first and then client. +* To close connection, give keyboard interrupt to client, server will close automatically. ## Features + You will find follwing on top of vanilla implementation you find in any first tutorial link you get from googling: + * Allowing communication with trusted client only, check using first received message from client as key message * Multithreading to handle multiple clients -* Use of pickel to serialize any type of data +* Use of pickel to serialize any type of data * Handling of variable length data by passing payload size along with payload * Passing data identifier with payload as additional information to take specific action accordingly (eg. if data identifier is image then save payload as image) * Using keyboard interrupt to close client and server cleanly ## Notes -* Program is wrtten in python 3.7.4 - -> *** Feel free to request more feature you would like, i wil try to add it when i get time *** +* Program is written in python 3.7.4 +> ***Feel free to request more feature you would like, i wil try to add it when i get time*** \ No newline at end of file diff --git a/cam_streaming/client.py b/cam_streaming/client.py new file mode 100644 index 0000000..01e6d4f --- /dev/null +++ b/cam_streaming/client.py @@ -0,0 +1,95 @@ +import socket +import pickle +import cv2 +import struct +import sys +import yaml + + +def send_data(conn, payload, data_id=0): + """ + @brief: send payload along with data size and data identifier to the connection + @args[in]: + conn: socket object for connection to which data is supposed to be sent + payload: payload to be sent + data_id: data identifier + """ + # serialize payload + serialized_payload = pickle.dumps(payload) + # send data size, data identifier and payload + conn.sendall(struct.pack(">I", len(serialized_payload))) + conn.sendall(struct.pack(">I", data_id)) + conn.sendall(serialized_payload) + + +def receive_data(conn): + """ + @brief: receive data from the connection assuming that + first 4 bytes represents data size, + next 4 bytes represents data identifier and + successive bytes of the size 'data size'is payload + @args[in]: + conn: socket object for conection from which data is supposed to be received + """ + # receive first 4 bytes of data as data size of payload + data_size = struct.unpack(">I", conn.recv(4))[0] + # receive next 4 bytes of data as data identifier + data_id = struct.unpack(">I", conn.recv(4))[0] + # receive payload till received payload size is equal to data_size received + received_payload = b"" + reamining_payload_size = data_size + while reamining_payload_size != 0: + received_payload += conn.recv(reamining_payload_size) + reamining_payload_size = data_size - len(received_payload) + payload = pickle.loads(received_payload) + return (data_id, payload) + + +def main(config): + # create client socket object and connect it to server + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.connect((config["ip"], config["port"])) + send_data(conn, config["key_message"]) + first_payload = receive_data(conn)[1] + if first_payload == "You are not authorized": + print("[ERROR]: Access denied") + else: + # send a cam data in loop till keyboard interrupt is received + cap = cv2.VideoCapture(0) + if not cap.isOpened(): + print("[ERROR]: Failed to open camera") + conn.close() + sys.exit() + while True: + try: + # send image + ret, frame = cap.read() + if not ret: + print("[ERROR]: Failed to read frame") + break + else: + if config["client"]["resize_image"]: + frame = cv2.resize( + frame, + ( + config["client"]["resized_width"], + config["client"]["resized_height"], + ), + ) + if config["client"]["grayscale"]: + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + send_data(conn, frame, config["data_identifiers"]["image"]) + # print(receive_data(conn)[1]) + except KeyboardInterrupt: + print("\n[INFO]: Keyboard Interrupt received") + # once keyboard interrupt is received, send signal to server for closing connection + send_data(conn, "bye") + # close connection + conn.close() + # print("[INFO]: Connection closed") + + +if __name__ == "__main__": + with open("config.yaml", "r") as file: + config = yaml.safe_load(file) + main(config) diff --git a/cam_streaming/config.yaml b/cam_streaming/config.yaml new file mode 100644 index 0000000..7df1c8a --- /dev/null +++ b/cam_streaming/config.yaml @@ -0,0 +1,28 @@ +ip: "127.0.0.1" +port: 12345 +# key to trust a connection +key_message: "C0nn3c+10n" +# define identifiers for data which could be used to take certain action for data +data_identifiers: + info: 0 + data: 1 + image: 2 +server: + #if true then visualization of streamed cam without latency will be done + show_normal_cam: True + #if true then visualization of streamed cam with latency will be done + show_cam_with_latency: True + # latency between visualization of two consecutive frame + latency: 0.2 + # maximum number of frames which should be accumulated in buffer before all frames as dropped + cam_buffer_len: 1000 + +client: + # if True then image from cam will be resized before sending + resize_image: False + # width of the resized image + resized_width: 1024 + # height of the resized image + resized_height: 512 + # if true the cam image will be converted to grayscale before sending + grayscale: False \ No newline at end of file diff --git a/cam_streaming/server.py b/cam_streaming/server.py new file mode 100644 index 0000000..1bff519 --- /dev/null +++ b/cam_streaming/server.py @@ -0,0 +1,135 @@ +import socket +import threading +import time +import pickle +import cv2 +import struct +import time +import yaml + + +def send_data(conn, payload, data_id=0): + """ + @brief: send payload along with data size and data identifier to the connection + @args[in]: + conn: socket object for connection to which data is supposed to be sent + payload: payload to be sent + data_id: data identifier + """ + # serialize payload + serialized_payload = pickle.dumps(payload) + # send data size, data identifier and payload + conn.sendall(struct.pack(">I", len(serialized_payload))) + conn.sendall(struct.pack(">I", data_id)) + conn.sendall(serialized_payload) + + +def receive_data(conn): + """ + @brief: receive data from the connection assuming that + first 4 bytes represents data size, + next 4 bytes represents data identifier and + successive bytes of the size 'data size'is payload + @args[in]: + conn: socket object for conection from which data is supposed to be received + """ + # receive first 4 bytes of data as data size of payload + data_size = struct.unpack(">I", conn.recv(4))[0] + # receive next 4 bytes of data as data identifier + data_id = struct.unpack(">I", conn.recv(4))[0] + # receive payload till received payload size is equal to data_size received + received_payload = b"" + reamining_payload_size = data_size + while reamining_payload_size != 0: + received_payload += conn.recv(reamining_payload_size) + reamining_payload_size = data_size - len(received_payload) + payload = pickle.loads(received_payload) + return (data_id, payload) + + +def handle_client(conn, conn_name, config): + """ + @brief: handle the connection from client at seperate thread + @args[in]: + conn: socket object of connection + con_name: name of the connection + config: configuration + """ + cam_buffer = [] + timer = time.time() + while True: + data_id, payload = receive_data(conn) + # if data identifier is image then handle the image + if data_id == config["data_identifiers"]["image"]: + # show normal cam + if config["server"]["show_normal_cam"]: + cv2.imshow("normal_stream", payload) + # show cam with latency + if config["server"]["show_cam_with_latency"]: + if len(cam_buffer) > config["server"]["cam_buffer_len"]: + cam_buffer = [] + cam_buffer.append(payload) + current_time = time.time() + if current_time - timer > config["server"]["latency"]: + cv2.imshow("stream_with_latency", cam_buffer[0]) + cam_buffer.pop(0) + cv2.waitKey(1) + timer = current_time + # send response to client + send_data(conn, "Image received on server") + else: + # if data is 'bye' then break the loop and client connection will be closed + if payload == "bye": + print("[INFO]: {} requested to close the connection".format(conn_name)) + print("[INFO]: Closing connection with {}".format(conn_name)) + break + else: + print(payload) + conn.close() + cv2.destroyAllWindows() + + +def main(config): + # create server socket + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_socket.bind((config["ip"], config["port"])) + server_socket.listen(5) + print("[INFO]: Server Started") + + while True: + try: + # accept client connection + # if first message from client match the defined message + # then handle it at separate thread + # otherwise close the connection + conn, (address, port) = server_socket.accept() + conn_name = "{}|{}".format(address, port) + print("[INFO]: Accepted the connection from {}".format(conn_name)) + first_payload = receive_data(conn)[1] + if first_payload == config["key_message"]: + print("Connection could be trusted, starting communication") + send_data(conn, "Connection accepted") + threading.Thread( + target=handle_client, + args=(conn, conn_name, config), + ).start() + break + else: + print( + "[WARNING]: Accepted connection is an unknown client, \ + closing the connection" + ) + send_data(conn, "You are not authorized") + conn.close() + # break the while loop when keyboard interrupt is received and server will be closed + except KeyboardInterrupt: + print("\n[INFO]: Keyboard Interrupt Received") + break + server_socket.close() + # print("[INFO]: Server Closed") + + +if __name__ == "__main__": + with open("config.yaml", "r") as file: + config = yaml.safe_load(file) + main(config) diff --git a/client.py b/client.py index ac2cc2c..4cc747e 100644 --- a/client.py +++ b/client.py @@ -77,7 +77,7 @@ def main(): send_data(conn, "bye") # close connection conn.close() - print("[INFO]: Connection closed") + # print("[INFO]: Connection closed") if __name__ == "__main__": diff --git a/conda_env_export.yml b/conda_env_export.yml new file mode 100644 index 0000000..2f72bbf Binary files /dev/null and b/conda_env_export.yml differ diff --git a/docs/TODO.md b/docs/TODO.md new file mode 100644 index 0000000..9202194 --- /dev/null +++ b/docs/TODO.md @@ -0,0 +1,3 @@ +# TODO +* catching keyboard interrupt is not working in server.py. For now as a workaround break is added to while loop after connecting with a client. Need to be fixed. +* close client in a neat way. \ No newline at end of file diff --git a/server.py b/server.py index ae2343a..3a1e462 100644 --- a/server.py +++ b/server.py @@ -137,7 +137,7 @@ def main(): break server_socket.close() - print("[INFO]: Server Closed") + # print("[INFO]: Server Closed") if __name__ == "__main__":