시작하며
지난번까지 직접 만든 크립토 라이브러리에 대한 설명을 작성하였고, 이번에는 이 라이브러리를 이용하여 안전하게 키를 교환하여 통신을 하는 프로그램에 대해 설명하겠다. 암호화와 보안과 관련된 설명은 이번 글에서는 다루지 않고, 이번 글에서는 멀티쓰레드 기반 소켓 프로그램을 어떻게 구현하였는지 먼저 살펴보겠다. 이 소켓프로그램은 다른 통신이 그렇듯, 보낼 메시지의 크기를 먼저보내고, 그 크기에 해당하는 데이터를 보낸다. 크기를 먼저 보내 상대방이 데이터를 다 받았는지 안 받았는지 검증할 수 있다. 또 글을 읽기전에 중요한 점은, 이 프로그램은 하나의 서버에 다수의 클라이언트가 접속하여 서로 오픈채팅방처럼 채팅을 하는 프로그램이라는 것을 알아두자.
1. Server
먼저 소켓을 생성, 초기화 이후 bind, listen을 호출한다. 이러한 소켓프로그램의 흐름을 잘 모를 경우, 아래 참고하기 좋은 글이 있어 링크로 남겨둘테니 보면 좋을 것 같다.
https://velog.io/@minji/소켓프로그래밍-bind-listen-accept
[네트워크] 서버의 시스템 콜( bind, listen, accept )
지난 글에서 serv_addr 구조체에 값을 채워넣음으로써 소켓의 ip주소와 port번호를 지정해주었다.다음으로, 실제 소켓의 할당에 해당하는 bind함수, 클라이언트의 연결요청을 기다리는 listen함수, 연
velog.io
소켓을 listen 상태로 만들면 while문으로 계속해서 소켓의 접속을 감시한다. 소켓 연결 요청이 온다면 accept로 client 소켓을 생성한다.
이렇게 소켓을 생성하면, clnt_socks 배열에 생성한 클라이언트 소켓을 인덱스에 맞게 넣는다. 여기서 주의할 점은 이 108번 행을 수행하기 위해 뮤텍스 락을 걸었다는 것이다. 멀티쓰레드기반으로 프로그래밍을 할 때 굉장히 중요한 건데, 뮤텍스 락이 걸려있는 곳은 저 코드가 실행될때까지 아무도 접근할 수 없다. 즉 멀티쓰레드는 동시에 여러 작동이 일어나지만, 저 mutext_lock 처리가 되어있는 부분은 unlock 될때까지 아무도 침범할 수 없다. 이렇게 만든 이유는 소켓은 만들어진 순서대로 clnt_socks 배열에 들어가야 한다. 그러나 다른 쓰레드와 충돌이 일어나서 108번 행이 동시에 실행됐다고 가정하자. 그렇다면 똑같은 인덱스에 두 개의 소켓이 들어갈 수도 있고, 혹은 그 인덱스에 값을 넣으려고 하는데 갑자기 인덱스가 증가하는 경우 등, 여러가지 문제가 발생할 수 있다. 보통 저렇게 ++같은 증감 연산들이 있을 경우, 뮤텍스 처리를 하는 것 같다. 왜냐하면 동시에 일어났을 때 문제가 발생하기 가장 쉬운 구조이기 때문이다.
그러고 나서 pthread_create로 쓰레드를 생성하고 해당 클라이언트에 대한 쓰레드가 종료되면 바로 종료하도록 p_thread_detach함수를 사용한다. detach 함수는 쓰레드를 메인쓰레드에서 분리시키고 백그라운드에서 동작하게 하는데, 밑에 클라이언트에 대한 설명에서 join이 나올때 다시 설명하겠다.
hanle 클라이언트 함수를 보면 사용자에게 EOF, 즉 종료신호를 보낼때까지 특정 동작을 반복한다. 179번부터 while문을 보자. 클라이언트로부터 받은 메시지 길이와, 실제 메시지 크기가 같다면(이때, 서버는 암호화된 메시지를 전송받고 복호화는 진행하지 않는다. 이것에 대해선 다음 글에서 설명) 이를 hexdump로 출력하고 send_msg함수를 호출한다. send_msg는 해당 메시지를 보낸 클라이언트를 제외한 모든 클라이언트들에게 메시지를 전송하는 함수이다. 이 프로그램이 오픈 채팅방 느낌이라 어떤 클라이언트가 보낸 메시지를 모두에게 전달해야 한다. 만약 client가 접속 종료 신호를 보낸다면 195번에 mutex_lock을 걸고 clnt_socks 배열에 해당 소켓 자리에 배열 뒤부터 순차적으로 당겨온 후, 205번 행으로 클라이언트 수를 줄임으로써 해당 클라이언트와의 소켓을 208번 close로 종료하게 된다.
위는 send_msg 함수로 218번 행의 반복문 블록을 보면 219번, 메시지를 보낸 소켓을 제외(자기가 보낸 메시지를 자기가 받을 필요가 없으므로)한 모든 클라이언트에게 메시지 길이와 메시지를 보내는 것을 볼 수 있다.
즉 server.c는 먼저 서버 소켓을 생성한 후, 클라이언트 접속이 오면 접속을 요청한 클라이언트에 대한 소켓을 생성하여 연결하고 해당 소켓에 대한 쓰레드를 생성한 뒤, 이 쓰레드를 detach로 백그라운드로 보내고 계속 while문으로 돌면서 새로운 클라이언트의 접속을 감시하고 연결한다. 또 연결된 클라이언트에 대한 처리는 handle_clnt 함수로 다루는데, 이 handle_clnt는 클라이언트가 보낼 메시지의 길이와 실제 보낸 데이터의 길이가 일치하고, 여타 오류가 없다면 send_msg()로 다른 클라이언트 소켓에 메시지를 전달하고, 만약 클라이언트가 종료신호를 보낸다면 handle_clnt는 해당 소켓을 clnt_socks 배열에서 제거하고, 전체 클라이언트 개수를 줄이면서 최종적으로 close로 해당 소켓에 대한 연결을 종료한다.
2. 클라이언트
먼저 client.c에서 클라이언트의 정보를 갖고 있는 ClietSockInfo 구조체이다. 멤버변수로는 소켓을 저장하는 sock, 그리고 채팅방 닉네임 name, 암복호화에 필요한 대칭키 key, 암복호화에 필요한 I_CIPHER_PARAMETERS(이전 i_crypto_library에 대한 설명 중에 나와있습니다.) 그리고 thread 종료를 위한 flag가 있다.(msg는 실제로 사용되지 않는데 아직 안지웠다...)
init_ClientSockInfo는 ClientSockInfo 구조체를 초기화한다.
먼저 인자로 받은 IP, port, name으로 자신의 이름과 자기 자신에 대한 소켓, 그리고 서버에 대한 초기화를 진행하고, 해당 ip, port에 해당하는 서버와 접속을 진행한다.
접속에 성공하면, send와 recv에 해당하는 쓰레드를 각각 만들고, 이후 각각에 쓰레드에 대해 join 함수를 사용한다.
이때 141번의 if문 블록은 recv쓰레드는 종료를 언제할 지 정할 수 없고 send만 자신이 종료에 대한 의사를 나타낼 수 있다. 즉 send함수에서 자신이 종료 신호를 보냈다면 flag를 1로 바꿔 recv_thread도 종료시키는 것이다. 여기서 join과 detach의 차이에 대해서 알아야하는데, 먼저 137, 138과 같이 쓰레드 생성만 하고 join을 하지 않는다면, 바로 147이 실행되고 메인함수가 종료될 것이다. 왜냐하면 생성만 하고 바로 다음 코드들을 실행하기 때문이다. 그러나 join은 해당 쓰레드가 종료될 때까지 기다린다는 의미로, 쓰레드가 끝날때까지 이 후 코드를 실행하지 않아 소켓이 닫히는 것을 방지할 수 있다. 이후 쓰레드가 종료되면 자원을 반환한다. 이와 달리 서버에서 detach를 한 이유는 쓰레드 자원은 쓰레드 종료 시 반환되어야 하지만, join을 할 경우, while문으로 계속해서 클라이언트의 접속을 받아야 하는데, 쓰레드가 종료될때까지 이후 코드를 실행하지 않아 반복이 멈추게 된다. 즉 detach는 이러한 쓰레드 종료를 기다리지 않고 백그라운드로 쓰레드가 돌아가게 하고, 계속 while문이 돌면서 클라이언트의 접속을 받을 수 있게 한다. detach와 join을 꼭 적절히 사용할 수 있어야 한다.
위는 send_msg 함수의 일부로 232번부터 236번은 맨처음 서버에 연결되었다면 Connected 메시지를 보낸다. 이 후 자신이 보내는 메시지에 대한 처리는 238번부터이다. while문으로 계속 입력을 받는다. 247번 행을 보면 q나 Q를 입력하면 Closed 메시지를 보내고 flag를 1로 바꾼 뒤 반복문을 빠져나와 함수를 종료시킨다. 즉 send 쓰레드가 종료되게 되고, flag가 1이 되었으므로 recv쓰레드도 종료되게 된다. 종료메시지가 아닌 일반적인 메시지라면 메시지와 이름을 합친 크기를 보내고, 자신의 이름과 함께 메시지를 암호화하여 메시지를 전송한다. 이 암호화 과정에 대해서는 다음 글에서 설명하겠다.
위는 recv_msg의 일부로, while문을 돌면서 계속해서 데이터를 읽는다. 우선 데이터 길이를 먼저 받고 실제 메시지 길이와 같은지 비교한다. 오류가 발생하면 쓰레드를 함수를 종료하고 오류가 없다면 데이터를 복호화하여 화면에 출력한다.
client.c는 ip, port로 서버에 접속하고, 자신의 닉네임과 전송할 데이터를 함께 암호화하여, 이 암호화한 데이터의 크기를 먼저 보낸 후, 암호화된 데이터를 전송한다. 받는 쪽에서는 데이터의 크기를 검증하고, 크기가 맞다면 데이터를 복호화하여 화면에 상대방이 보낸 메시지를 출력한다.
마치며
이번 글에서는 멀티쓰레드 기반으로 소켓프로그램을 구현하였다. 소켓 프로그램의 전반적인 흐름과 쓰레드와 관련한 함수들에 대한 이해가 안된다면 직접 공부해보는 것도 좋을 것이다. 실제 상용프로그램은 대부분 멀티쓰레드 기반이기 때문에 쓰레드에 대한 개념을 알고 있는 것은 굉장히 중요하다. 또 통신에서 데이터를 보내기전, 데이터의 크기를 먼저 보내는 것이 중요하다. 상대방이 데이터가 모두 왔는지 오지 않았는지 판단할 수 있어야 하기 때문이다. 네트워크 상의 혼잡으로 데이터가 끊겨서 전송될 수도 있기 때문이다. 이러한 팁들을 이용하여 소켓 프로그래밍을 연습하면 큰 도움이 될 것 같다. 오늘은 전반적인 흐름을 알아봤고, 다음 글에서 소켓 프로그래밍에서 중요한 select 함수를 이 프로그램에서 어떻게 사용했는지 간단하게 다뤄보고, 그 다음 글에서 보안 요소들에 대해 알아보겠다.
오픈채팅 형식의 소켓 프로그램 기반이 되었던 참고자료 : https://novice-programmer-story.tistory.com/38
Ch 18. 멀티쓰레드 기반의 서버구현
모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
novice-programmer-story.tistory.com
github : https://github.com/0xGh-st/I_CRYPTO.git
GitHub - 0xGh-st/I_CRYPTO
Contribute to 0xGh-st/I_CRYPTO development by creating an account on GitHub.
github.com
'C > Crypto' 카테고리의 다른 글
Crypto Project : 2-3. TLS방식을 모방한 소켓프로그램 (0) | 2023.05.05 |
---|---|
Crypto Project : 2-2. 소켓에서 select() 함수를 이용한 recv_all() 함수 (0) | 2023.04.26 |
Crypto Project : 1-6. crypto library makefile과 테스트 (0) | 2023.04.23 |
Crypto Project : 1-5. 암복호화 init, update, final 구현 (0) | 2023.04.16 |
Crypto Project : 1-4. C언어에서 멤버변수 접근을 막는 방법과 init, update, final의 개념 (0) | 2023.04.15 |