본문 바로가기

프로그래밍/코딩(C & SOCKET)

[150909] 리눅스 기반 채팅 chat_sv.c - 1

반응형

[150909] 리눅스 기반 채팅 sv_socket.c


 지난 번과 마찬가지로 채팅 프로그램이다. 다소 다른 점이 있다면, 단방향 통신에서 양방향 통신으로 바뀌었고 계속해서 기능을 붙여나갈 거라는 정도..? 학기 중에 같이하려니 여간 바쁜게 (귀찮은게) 아니다... 이것만 붙잡고 있을 수도 없는 노릇이니.. 안그래도 이번 학기에 C와 JAVA를 동시에 진행한다. 그 외에 수학과목들과 도대체가 목적을 알 수 없는 오토마타 수업까지.. 바빠서 포스팅이 늦어진다고 또 합리화해본다. 지난 포스팅에서 등장했던 소켓과 각종 함수들을 이해하자마자, 양방향 통신에서는 또 다른 개념들이 등장한다. 주요 개념을 몇 개 집어보라하면, 개인적으로는 FD_SET 구조체와 select() 함수를 예로 들 수 있겠다.


아, 그리고 소켓 통신에서는 소켓이 특정 상태에 돌입하고, 대기하고, 변화를 감지하면 접속을 시작하는 등의 방식으로 연결이 진행되는데, 포스팅 방식은 이러한 특정 상태를 하나 하나 구분지어 이에 대한 보충 설명을 하고, 마지막에 전체 코드를 넣어서 대략적인 흐름을 기술해볼까한다.





서버의 소켓을 생성한다. 서버에서 다루는 소켓의 대장 쯤이라고 이해하면 되겠다. 이 소켓을 보고 "랑데뷰 소켓"이라고도 한다.

생성한 소켓은 다음과 같은 정보를 가지고 있다.


PF_INET : IPv4 버전

SOCK_STREAM : TCP/IP 프로토콜

0 : 특정 프로토콜 사용 (없을 경우 0)


 그리고 if 문에서 소켓 생성에 실패할 경우 음수를 반환하는데, 이를 이용해서 소켓 생성에 실패할 경우 에러문을 출력, 종료하는 모습이다. 다시 소켓을 생성하거나 분기문을 돌아가는 등의 코드도 가능하다.




소켓을 생성했다면, 이에 대한 정보를 가지고 있는 구조체를 만들어야한다. 이 구조체는 이미 정의되어있는 상태이며 일반적으로 소켓 통신 등에 자주 쓰인다.

현재 코드에 써있지 않지만, 상단에 구조체를 선언해줘야한다.




바로 위에 코드는 sockaddr_in 의 구조체 내용이다. 다시 memset... 코드로 돌아가보면, 순서는 memset() 함수를 이용해서 sv_sock 구조체의 내용을 0으로 모두 초기화시키고 아래 3줄로 각 구조체의 인자를 참조해서 정보를 삽입하고 있다. 참조하는 방법은 간단하다.


[구조체 변수명].[해당 인자] = [대입할 정보]


이 구조체의 이름을 sv_sock으로 정했고, 이제 sockaddr_in ( = sv_sock) 은 주소 체계나 스트림 등을 담고 있는 ( = 서버 소켓에 대한 모든 정보를 가지고 있는) 구조체가 됐다.


이제 서버 소켓과 관련된 설정은 끝났다. 이 후 순서를 미리 좀 보자면, 지금 생성한 서버 소켓과 앞으로 추가할 클라이언트 소켓을 통해서 통신을 진행한다. 그런데, 이렇게만 하면 동시 양방향 다중 통신이 불가능하므로, 서버 소켓 ( = 랑데부 소켓) 에서 각 클라이언트 소켓과 대응되는 커뮤티케이션 소켓을 생성한다. 커뮤니케이션 소켓은 각 클라이언트 소켓과 통신하고, 서버 소켓으로부터 생성된 여러 개의 커뮤니케이션 소켓은 또 서로 통신하며 마치 서버와 다수의 클라이언트가 동시에 통신하는 듯한 효과를 낼 수 있다.




다음 과정은 지난 번 포스팅에서 다룬 bind()이다. 간략하게만 다시 설명하고 넘어가자면, bind는 프로그램이 알고 있는 소켓 번호와 외부 프로토콜이 알고 있는 소켓 주소를 묶어서 (bind) 원활한 통신이 가능하도록 정보를 제공한다. 마찬가지로 bind에 실패하면 음수를 반환하고, 이를 if문에 걸어서 에러를 체크한다.




기본 세팅을 마치고, 클라이언트의 접속 까지 대기하는 Listen 함수이다. 당연히, 해당 소켓이 lsiten 모드로 들어가므로 첫 번째 인자에 서버 소켓 넘버를 넣어주고 두 번째 인자에서는 최대로 연결할 수 있는 클라이언트 수를 설정한다. 적당히 해주면 된다. 본 코드에서는 5로 설정했다. 이 역시 실패시 음수를 반환하며, 연결을 종료하도록 설정했다.





 길다... While 문은 아직 닫혀 끝나지 않은 상태고, select 까지만 일단 잘라서 코드를 분석해보자. for 문 전까지의 세 줄을 보면 FD (파일 디스크립터) 구조체를 조작하는 함수를 사용하고 있다.


+ 파일 디스크립터는 시스템에서 특정 파일이나 소켓을 대표하는 정수라고 생각하면 된다. 이 디스크립터와 연결되는 fd_set 구조체에서는 각 인덱스의 값이 0 또는 1로 표현되는데, 1로 표현되는 비트가 존재할 경우, "아, 이곳에 변화가 있구나, 그런데 인덱스가 0 인걸 보니 표준입력과 관련된 변화가 발생했구나!" 라고 인식하고 해당 소켓과 통신을 시작한다.


이 구조체의 0,1,2 인덱스 안에는 각 각 표준 입력 / 표준 출력  / 표준 에러와 관련된 장치가 이미 자리잡고 있다. 그러므로 실질적으로 우리가 다루는 첫 번째 인덱스는 3 부터이다.


FD_ZERO : 인자로 들어온 해당 주소에 속하는 변수를 0으로 초기화한다.

FD_SET(int fd, &fdad) : 첫 번째 인자의 정수 인덱스의 비트를 1로 설정한다. 인덱스는 두 번째 들어온 인자 배열(구조체)의 인덱스를 말한다.


status ( = fd_set 구조체, 초기 코드에서 선언한 변수이름임. )구조체를 초기화시키고, 입력 감지와 서버소켓 감지를 위해서 각 FD를 SET했다. 다음 반복문에서는 현재 연결된 클라이언트 소켓 전체의 각 소켓 번호를 status 구조체에 FD_SET을 이용하여 저장하고, 만약 최대 용량을 넘는다면 1씩 늘려서 보완해준다.




여기부터는 이제 특정 소켓을 가진 status에 해당 인덱스의 비트가 1인지 체크해서 동작을 수행한다. 위의 분기문 같은 경우, 서버 소켓이 있는 인덱스의 비트 값이 1인지 체크하고, 맞으면 accept를 진행한다. 추가적으로 연결되는 클라이언트들을 다룬 코드이다. cl_num[]은 랑데부 소켓으로부터 나온, 각 클라이언트들과 통신하기 위해 만들어진 커뮤니케이션 소켓이다. 이 후에 현재 연결된 클라이언트 숫자를 늘려주며 연결 상황을 출력하고 있다.




다음 분기문은 키보드의 입력을 감지한다. 위에서 설명했지만, status에서 표준 입력 인덱스의 비트가 1인지 체크하고, 맞다면 분기문 안으로 들어가 키보드 입력과 관련된 명령을 수행한다. 분기문 안의 내용은 간단한데, 표준 입력(0)으로 입력되는 문자열을 버퍼 ucBuff 에 저장시키고 읽어들인 바이트 수를 connect_ch 에 저장한다. 그리고 반복문을 통해서 접속해있는 모든 소켓에게 해당 버퍼 내용을 출력시키고 있다.




모든 커뮤니케이션 소켓에 대해서 버퍼에 저장된 데이터를 송신한다. 물론 자신에게도. 분기문은 계속해서 같은 내용이 반복된다. 해당 소켓에 변화가 일어 났는지. 일어났다면 통신을 시작하고 어떠한 동작을 수행할 것인지. 등의 내용으로 말이다.




무한 루프를 도는 While문이 끝난 이후의 코드이다. 버퍼의 첫 번째 입력된 값이 q라면 소켓을 종료하며, 그 외에는 별다른 수행 없이 while문이 종료되었다면 서버 소켓을 종료하고, 나머지 연결된 모든 커뮤니케이션 소켓을 차례로 종료한다.


일단 채팅서버의 주요 코드 내용들은 모두 짚어봤다. 사실상 거의 다 써놨는데, 다음 포스팅에서는 클라이언트의 코드를 분석해볼 것이다. 상대적으로 연결 과정도 짧고, while문의 반복내용이 주로 데이터 처리 과정이기 때문에, FD_SET 구조체 조작 함수들만 이해하고 있다면 상대적으로 코드 내용이 간단하다. 클라이언트 코드 포스팅 이후에는 소스 코드 파일을 게시하고, 시간 나는대로 기능이 추가된다면 수정할 계획이다.




반응형