앞서 서버 편에서 거의 대부분의 설명을 마쳤기 때문에, 클라이언트편은 내용이 다소 짧게 느껴지실 수도 있습니다 (...)

그래도 서버/클라이언트 편을 잘 이해하신다면, 충분히 채팅 프로그램을 만들고, 더 나아가서는 응용하여 파일 전송까지도 가능하리라 봅니다. (Socket 에서 SendFIle을 지원해서 간단하게 구현하는 것도 가능하지만, 데이터를 분할해서 보내는 것을 익히는 것도 좋은 경험이 될 거라 생각합니다!)


그럼 거두절미 하고 바로 본론으로 들어가보도록 하겠습니다!


Server

Client

 [1] Bind - Listen - BeginAccept 순서대로 호출

 [2] Connect/BeginConnect 이용하여 연결 시도

 [3] 연결 요청이 있다면, EndAccept 로 연결 수락을 한 후 BeginReceive 로 자료 수신 대기

 [4] 연결에 성공했다면, BeginReceive 로 자료 수신 대기, Send/BeginSend 이용하여 자료를  전송

 [5] 수신한 자료가 있을 경우, EndReceive 로 자료를 수신하고 다시 BeginReceive 호출하여 자료 수신 대기

(서버 편의 표를 그대로 붙여왔습니다 ....)


클라이언트의 1번째 단계!

Connect/BeginConnect 를 이용한 연결 시도.

비동기를 다루기 때문에 BeginConnect를 이용하여 연결 시도를 해야 하지만, BeginConnect의 경우 타임아웃을 처리하기가 까다로워 Connect를 설명하도록 하겠습니다.

그리고 Connect 메서드의 장점은 연결이 성립된 경우 즉시 제어를 돌려주기 때문에 윈도우 폼에서 소켓 통신 프로그램을 만들 때 유용하게 사용되기도 합니다.


그럼 Connect 메서드에 대한 설명입니다.

Connect 메서드 (Socket.Connect 메서드 문서, MSDN)

사용되는 매개 변수는 두 개입니다. 각각의 매개 변수의 설명입니다.

host - 호스트 이름을 입력합니다.

port - 포트 번호를 입력합니다.


사용법은 아래와 같습니다.

Boolean isConnected = false;
try {
    // 연결 시도
    Socket.Connect("hostName", portNumber);
    
    // 연결 성공
    isConnected = true;
catch {
    // 연결 실패 (연결 도중 오류가 발생함)
    isConnected = false;
}


Connect 메서드의 경우, 연결에 실패하면 예외를 발생시키게 됩니다.

그래서 try ~ catch 문으로 예외 처리를 함으로써 연결 성공/실패 여부를 나타나게 하였습니다.


Tip!

예외처리란, 발생하는 예외에 대한 제어를 처리하는 것으로써 오류가 날만한 명령이나 구문이 있는 부분을 try ~ catch 으로 감싸주어 발생하는 오류를 미연에 방지하고, 오류에 대한 내용을 추적함으로써 오류 수정 및 프로그램 실행을 더욱 유연하게 해주는 아주 중요하고 좋은 프로그램을 만드는데 아주 중요한 방법입니다.


그럼 Connect 메서드를 이용한 연결 코드를 보시겠습니다.

private Socket m_ClientSocket = null;
 
public void ConnectToServer(String hostName, UInt16 hostPort) {
    // TCP 통신을 위한 소켓을 생성합니다.
    m_ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
 
    Boolean isConnected = false;
    try {
        // 연결 시도
        m_ClientSocket.Connect("hostName", portNumber);
        
        // 연결 성공
        isConnected = true;
    } catch {
        // 연결 실패 (연결 도중 오류가 발생함)
        isConnected = false;
    }
    
}


ConnectToServer 메서드를 호출하면 소켓의 새 개체를 만들고, Connect 메서드를 이용해 연결을 시도하고 있습니다.

연결에 성공하면 바로 2번째 단계로 넘어가서, 자료 수신을 대기하고 자료를 전송하는 부분을 구현하면 됩니다.


자료 수신 대기 부분은 try ~ catch 문 밖에 isConnected 변수를 확인하고 true일 경우에 BeginReceive 를 호출하면 되겠습니다. 코드로 구현하면 이렇게 나타낼 수 있습니다.

public class AsyncObject {
    public Byte[] Buffer;
    public Socket WorkingSocket;
    public AsyncObject(Int32 bufferSize) {
        this.Buffer = new Byte[bufferSize];
    }
}
 
private AsyncCallback m_fnReceiveHandler = new AsyncCallback(handleDataReceive);
public void ConnectToServer(String hostName, UInt16 hostPort) {

    /* 이전의 코드 생략 */
    
    if ( isConnected ) {

        // 4096 바이트의 크기를 갖는 바이트 배열을 가진 AsyncObject 클래스 생성
        AsyncObject ao = new AsyncObject(4096);
     
        // 작업 중인 소켓을 저장하기 위해 sockClient 할당
        ao.WorkingSocket = m_ClientSocket;
     
        // 비동기적으로 들어오는 자료를 수신하기 위해 BeginReceive 메서드 사용!
        m_ClientSocket.BeginReceive(ao.Buffer, 0, ao.Buffer.Length, SocketFlags.None, m_fnReceiveHandler, ao);
        
    } else {
        throw new InvalidOperationException("연결에 실패했습니다.....");
    }
}
private void handleDataReceive(IAsyncResult ar) {
 
    // 넘겨진 추가 정보를 가져옵니다.
    // AsyncState 속성의 자료형은 Object 형식이기 때문에 형 변환이 필요합니다~!
    AsyncObject ao = (AsyncObject) ar.AsyncState;
 
    // 자료를 수신하고, 수신받은 바이트를 가져옵니다.
    Int32 recvBytes = ao.WorkingSocket.EndReceive(ar);
 
    // 수신받은 자료의 크기가 1 이상일 때에만 자료 처리
    if ( recvBytes > 0 ) {
        /*
            여기에 자료를 처리하는 작업을 하시면 됩니다.
        */
    }
 
    // 자료 처리가 끝났으면~
    // 이제 다시 데이터를 수신받기 위해서 수신 대기를 해야 합니다.
    // Begin~~ 메서드를 이용해 비동기적으로 작업을 대기했다면
    // 반드시 대리자 함수에서 End~~ 메서드를 이용해 비동기 작업이 끝났다고 알려줘야 합니다!
    ao.WorkingSocket.BeginReceive(ao.Buffer, 0, ao.Buffer.Length, SocketFlags.None, m_fnReceiveHandler, ao);
}



클라이언트는 여기가 끝입니다.

생각하신것보다 코드가 다소 짧아 놀라셨겠지만, 확실히 매우 간단하고 짧은 코드로 채팅 프로그램을 구현하실 수 있습니다.


그럼.. 클라이언트의 전체 소스입니다!

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
 
namespace ChatClient {
    public class ChatClient {
        public class AsyncObject {
            public Byte[] Buffer;
            public Socket WorkingSocket;
            public AsyncObject(Int32 bufferSize) {
                this.Buffer = new Byte[bufferSize];
            }
        }
         
        private Boolean g_Connected;
        private Socket m_ClientSocket = null;
        private AsyncCallback m_fnReceiveHandler;
        private AsyncCallback m_fnSendHandler;

        public ChatClient() {

            // 비동기 작업에 사용될 대리자를 초기화합니다.
            m_fnReceiveHandler = new AsyncCallback(handleDataReceive);
            m_fnSendHandler = new AsyncCallback(handleDataSend);

        }
        
        public Boolean Connected {
            get {
                return g_Connected;
            }
        }

        public void ConnectToServer(String hostName, UInt16 hostPort) {
            // TCP 통신을 위한 소켓을 생성합니다.
            m_ClientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP);
         
            Boolean isConnected = false;
            try {
                // 연결 시도
                m_ClientSocket.Connect(hostName, hostPort);
                
                // 연결 성공
                isConnected = true;
            } catch {
                // 연결 실패 (연결 도중 오류가 발생함)
                isConnected = false;
            }
            g_Connected = isConnected;
            
            if ( isConnected ) {
        
                // 4096 바이트의 크기를 갖는 바이트 배열을 가진 AsyncObject 클래스 생성
                AsyncObject ao = new AsyncObject(4096);
             
                // 작업 중인 소켓을 저장하기 위해 sockClient 할당
                ao.WorkingSocket = m_ClientSocket;
             
                // 비동기적으로 들어오는 자료를 수신하기 위해 BeginReceive 메서드 사용!
                m_ClientSocket.BeginReceive(ao.Buffer, 0, ao.Buffer.Length, SocketFlags.None, m_fnReceiveHandler, ao);
                
                Console.WriteLine("연결 성공!");
                
            } else {
                
                Console.WriteLine("연결 실패!");
                
            }
        }
        
        public void StopClient() {
            // 가차없이 클라이언트 소켓을 닫습니다.
            m_ClientSocket.Close();
        }
         
        public void SendMessage(String message) {
            // 추가 정보를 넘기기 위한 변수 선언
            // 크기를 설정하는게 의미가 없습니다.
            // 왜냐하면 바로 밑의 코드에서 문자열을 유니코드 형으로 변환한 바이트 배열을 반환하기 때문에
            // 최소한의 크기르 배열을 초기화합니다.
            AsyncObject ao = new AsyncObject(1);
         
            // 문자열을 바이트 배열으로 변환
            ao.Buffer = Encoding.Unicode.GetBytes(message);
            
            ao.WorkingSocket = m_ClientSocket;
         
            // 전송 시작!
            try {
                m_ClientSocket.BeginSend(ao.Buffer, 0, ao.Buffer.Length, SocketFlags.None, m_fnSendHandler, ao);
            } catch (Exception ex) {
                Console.WriteLine("전송 중 오류 발생!\n메세지: {0}", ex.Message);
            }
        }
         
        private void handleDataReceive(IAsyncResult ar) {
         
            // 넘겨진 추가 정보를 가져옵니다.
            // AsyncState 속성의 자료형은 Object 형식이기 때문에 형 변환이 필요합니다~!
            AsyncObject ao = (AsyncObject) ar.AsyncState;
         
            // 받은 바이트 수 저장할 변수 선언
            Int32 recvBytes; 
            
            try {
                // 자료를 수신하고, 수신받은 바이트를 가져옵니다.
                recvBytes = ao.WorkingSocket.EndReceive(ar);
            } catch {
                // 예외가 발생하면 함수 종료!
                return;
            }
         
            // 수신받은 자료의 크기가 1 이상일 때에만 자료 처리
            if ( recvBytes > 0 ) {
                // 공백 문자들이 많이 발생할 수 있으므로, 받은 바이트 수 만큼 배열을 선언하고 복사한다.
                Byte[] msgByte = new Byte[recvBytes];
                Array.Copy(ao.Buffer, msgByte, recvBytes);
                
                // 받은 메세지를 출력
                Console.WriteLine("메세지 받음: {0}", Encoding.Unicode.GetString(msgByte));
            }
         
            try {
                // 자료 처리가 끝났으면~
                // 이제 다시 데이터를 수신받기 위해서 수신 대기를 해야 합니다.
                // Begin~~ 메서드를 이용해 비동기적으로 작업을 대기했다면
                // 반드시 대리자 함수에서 End~~ 메서드를 이용해 비동기 작업이 끝났다고 알려줘야 합니다!
                ao.WorkingSocket.BeginReceive(ao.Buffer, 0, ao.Buffer.Length, SocketFlags.None, m_fnReceiveHandler, ao);
            } catch (Exception ex) {
                // 예외가 발생하면 예외 정보 출력 후 함수를 종료한다.
                Console.WriteLine("자료 수신 대기 도중 오류 발생! 메세지: {0}", ex.Message);
                return;
            }
        }
        private void handleDataSend(IAsyncResult ar) {
         
            // 넘겨진 추가 정보를 가져옵니다.
            AsyncObject ao = (AsyncObject) ar.AsyncState;
            
            // 보낸 바이트 수를 저장할 변수 선언
            Int32 sentBytes;
            
            try {
                // 자료를 전송하고, 전송한 바이트를 가져옵니다.
                sentBytes = ao.WorkingSocket.EndSend(ar);
            } catch (Exception ex) {
                // 예외가 발생하면 예외 정보 출력 후 함수를 종료한다.
                Console.WriteLine("자료 송신 도중 오류 발생! 메세지: {0}", ex.Message);
                return;
            }
         
            if ( sentBytes > 0 ) {
                // 여기도 마찬가지로 보낸 바이트 수 만큼 배열 선언 후 복사한다.
                Byte[] msgByte = new Byte[sentBytes];
                Array.Copy(ao.Buffer, msgByte, sentBytes);
                
                Console.WriteLine("메세지 보냄: {0}", Encoding.Unicode.GetString(msgByte));
            }
        }
    }
}


Connected 라는 속성을 하나 만듦으로써 연결에 성공했는지 실패했는지의 여부를 외부에서도 알 수 있도록 하였고,

소켓의 경우 서버에 연결할 때 즉시 생성하도록 했습니다.

그리고 대리자의 경우엔 클래스의 새 개체를 만들 때(생성자) 초기화되도록 변경하였고.. Connect 메서드를 이용하였습니다.


그리고 최종 클라이언트/서버 솔루션 파일입니다.

ChatApp.zip


마지막으로 클라이언트/서버 실행 테스트 결과입니다.


이 예제를 응용하시면 윈도우 폼에서도 채팅 프로그램을 구현하시는 것이 충분히 가능할 것이라 생각됩니다.

더 넘어서 파일 보내기/이미지 전송 등도 구현해 보시는게 어떨까요? ^^


긴 글 읽어주셔서 감사합니다!

그리고, 클라이언트 편이 다소 짧지만.. 양해 부탁드립니다 ㅠㅠ


+ Recent posts