Header

  1. View current page

    Haskell Programming Language

Profile_image?t=1224044209&type=small
Haskell 관련 자료 정리
7

기초 네트워크 프로그래밍 - 채팅 서버와 클라이언트

 

시작하기

하스켈 관련 자료를 보면 대부분 굉장히 이상적인 문제 풀이에 치중하는 경향이 있습니다. 그러다보니 '너무 수학적인 면이 강하다.', '좋아보이긴 한데 그다지 쓸모는...' 등의 반응이 나오죠.(뭐 사실 저도 하스켈이 그다지 보편적이고 실용적인 언어라고 생각하지는 않습니다만...ㅡ_ㅡ) 
어쨌든 아무래도 하스켈이 너무나 멋지다 보니 그런 착한 모습만을 소개해주고 싶은 욕심이 너무 과한 것이 아닌가 생각합니다. (워~ 워~)

그래서 전 욕심을 버리고 비교적 실용적인 예제를 통해 하스켈과 친해지려고 합니다. 그리고 그런 실용적인 예제 중간에 기회가 되면 하스켈의 멋진 모습을 살짝 살짝 보이도록 노력하겠습니다. 우선 간단한 네트워크 프로그래밍으로 시작합니다.
아래는 초간단 네트워크 서버 및 클라이언트 소스입니다.

 

  1. import Network    -- 1)
    import IO            -- 2)

    simpleServer = withSocketsDo simpleServer'    -- 3)
    simpleServer' = do
        sock <- listenOn (PortNumber 10000)            -- 4)
        (h,host,port) <- accept sock                        -- 5)
        putStrLn ("connected from " ++ host ++ ":" ++ (show port))
        contents <- hGetContents h            -- 6)
        putStrLn contents
        sClose sock                    -- 7)

  2. simpleClient = withSocketsDo simpleClient'
    simpleClient' = do
        sock <- connectTo "localhost" (PortNumber 10000)    -- 8)
        hPutStrLn sock "test"    -- 9)
        hClose sock        -- 10)

 

먼저 서버 소스 설명입니다.

1) 하스켈에서는 Network 라고 하는 모듈이 있습니다. 물론 이름 그대로 네트워크 프로그래밍에 관련된 함수 및 자료형을 제공합니다. 하스켈에서는 C/C++ 처럼 저 수준의 소켓을 직접 다룰 수 있는 함수를 제공하기도 합니다만 고 수준으로 래핑된 함수들도 제공합니다. (저 수준의 소켓을 다룰려면 import Network.Socket 이라고 하면 됩니다. 물론 이 때 사용되는 함수들이 따로 있습니다.)

2) 하스켈에서는 데이터를 주고 받기 위한 별도의 send/recv 함수를 제공하기도 하지만 일반 IO함수를 이용해서도 데이터 송/수신이 가능합니다. 이런 일반 IO 함수를 사용하기 위해 import IO 를 해줍니다.

3) 윈도우 시스템에서는 소켓 함수 사용 시 소켓 모듈 초기화 과정이 필요합니다. (아마 Win32 네트워크 프로그래밍을 해보신 분은 소켓 사용 시 맨 처음에 WSAStartup() 함수를 호출해야한다는 사실을 기억하실 것입니다. withSocketsDo 가 바로 그 역할을 합니다.) 윈도우가 아닌 다른 플랫폼에서 이것은 아무런 영향을 끼치지 않습니다. (따라서 플랫폼 호환성을 위해 항상 이것을 해주는 것이 좋습니다.) - 라고 GHC 문서에 설명이 되어 있긴 한데 제가 테스트를 해보니 꼭 안해줘도 별 문제가 없더군요...긁적긁적 (__ )a 

4) listen 은 해당 port 번호로 소켓을 하나 생성해 줍니다.

5) 클라이언트의 연결을 기다립니다. 접속 시도가 들어오면 (핸들, 호스트이름, 포트번호)로 된 자료형을 리턴합니다.(하스켈 공부해보신 분은 (h,host,port) 가 Triples 자료형이란 거 잘 아시죠?)

6) 5)에서 얻은 핸들값 h는 IO 작업을 위한 스트림 핸들값입니다. 따라서 이것을 가지고 IO 작업을 처리할 수 있습니다. 핸들을 이용한 IO 함수는 모두 h로 시작하며 해당 핸들 스트림으로 버퍼데이터를 받는 함수는 hGetContents입니다. 이 함수를 이용해서 클라이언트가 보낸 데이터를 받습니다.

7) 데이터를 화면에 출력하고 나면 소켓을 해제하고 종료합니다. sClose 함수는 리턴타입이 IO () 타입이므로 simpleServer' 가 do로 시작했더라도 return으로 끝날 필요가 없습니다.

이번에는 클라이언트 소스 설명입니다.

8) 함수명 그대로 인자로 받은 주소에 연결을 시도합니다. 성공하면 핸들값을 반환합니다.(실패하면 예외 발생)
9) 8)에서 받은 핸들값을 이용해서 "test"라는 문자열을 씁니다. hPutStrLn 은 첫번째 파라미터로 받은 핸들을 통해 두번째 파라미터로 받은 문자열을 출력합니다. 이 때 주어진 문자열 뒤에 '\n' 문자를 덧붙입니다.
10) 생성된 핸들을 해제합니다.(즉, 연결을 끊습니다.)

소스를 - 컴파일하기 귀찮으니 - 콘솔 두 개 띄워서 GHCi 인터프리터로 실행한 후 먼저 한 콘솔 창에서 simpleServer 함수를 띄우고 다른 콘솔 창에서 simpleClient 를 실행하면 다음과 같은 결과를 얻습니다.

 

사실 하스켈에서는 특정 서버에 접속에서부터 데이터 전송까지를 한번에 해결해주는 sendTo 라는 함수가 있습니다. 이 함수를 이용하면 클라이언트 소스는 좀 더 단순해집니다.

moreSimpleClient = withSocketsDo $ sendTo "localhost" (PortNumber 10000) "test"

이제 클라이언트 소스는 한줄로 줄어들었습니다. 물론 sendTo 의 반대 기능을 하는 recvFrom 함수도 있습니다.
만약 핸들이 계속 필요한 경우가 아니라면 이처럼 sendTo, recvFrom 을 사용하는게 더 편합니다.

다음 번에는 저 두 함수를 좀 더 업그레이드 해보도록 하겠습니다...

덧붙임1. 각 함수들의 세부 사항은 GHC 컴파일러 설치하면 있는 doc 폴더의 html 문서에 다소 불친절하게 나와 있으니 참고하세요...

덧붙임2. 하스켈의 기본 문법 및 함수들에 대한 설명은 귤님이 운영하시는 make it functional 사이트를 참고하시기 바랍니다.

 

 

간단한 클라이언트 프로그램 만들기

이번에는 클라이언트가 보내는 문자열을 계속해서 출력해주는 프로그램을 구현합니다. 클라이언트가 "bye"라는 문자열을 보내면 둘 다 접속을 종료합니다.
서버와 클라이언트 소스는 아래와 같습니다.

 

  1. import Network
    import IO

    simpleServer2 = withSocketsDo simpleServer2'
    simpleServer2' = do
        sock <- listenOn (PortNumber 10000)
        (h,host,port) <- accept sock
        putStrLn ("connected from " ++ host ++ ":" ++ (show port))
        catch (readLoop h) print    -- 1)
        putStrLn "disconnected"
        sClose sock

  2. readLoop h = do
        contents <- hGetLine h    -- 2)
        putStrLn contents
        if (contents /= "bye")         -- 3)
            then readLoop h
            else return ()
      
    simpleClient2 = withSocketsDo simpleClient2'
    simpleClient2' = do
        sock <- connectTo "localhost" (PortNumber 10000)
        writeLoop sock
        hClose sock
        putStrLn "disconnected"

    writeLoop h = do
        contents <- getLine    -- 4)
        hPutStrLn h contents
        hFlush h                    -- 5)
        if (contents /= "bye")
            then writeLoop h
            else return ()         -- 6)

 

simpleServer, simpleClient 함수와 많은 부분에서 중복되기 때문에 달라진 부분만 설명하자면...

1) catch 는 예외를 처리하는 함수입니다. 첫번째 파라미터로 받은 함수에서 예외가 발생하면 해당 예외 객체를 두번째 파라미터로 받은 함수에서 처리하도록 해줍니다. 따라서 여기서는 readLoop 함수가 예외가 발생하면 해당 예외를 print 함수가 받아서 화면에 에러 메시지를 출력하는 역할을 합니다. 이것을 C++로 표현하면 아래와 같습니다.

try {
    readLoop(h);
    }
catch (e)
    {
    print(e);
    }

2) hGetLine 함수는 hGetContents 처럼 파라미터로 주어진 스트림 핸들에서 데이터를 읽는 함수입니다. 단 hGetContents 는 버퍼 전체를 읽는 반면에 hGetLine 함수는 한줄씩 읽습니다.

3) /= 는 not equal 을 의미합니다. 소켓에서 읽은 문자열이 "bye"가 아니면 readLoop 함수를 재귀적으로 호출하며(하스켈에는 반복문이 없으며 재귀호출이 이것을 대신합니다.) "bye"이면 종료합니다.(readLoop 함수는 do 로 시작했고 반환해줘야 할 값이 없으므로 return () 로 종료합니다.)

4) 표준 입력(키보드)를 통해 한 줄씩 데이터를 읽습니다.

5) hFlush 는 주어진 스트림 핸들의 버퍼를 비우는 역할을 합니다. 이 함수를 호출하지 않으면 버퍼가 꽉차거나 연결이 종료될 때까지 데이터를 전송하지 않고 버퍼에 저장만 합니다. (이 함수를 주석처리하고 테스트를 해보면 그 차이를 알 수 있습니다.)

여기서 잠깐 하스켈의 모나드에 대해 잠깐 소개하자면...

원 래 하스켈의 함수들은 소스 코딩 순서와 실행 순서가 상관없습니다. 왜냐하면 하스켈에서는 상태(변수)란 것이 없으므로 함수의 실행 순서와 상관없이 단지 입력값에 의해서만 결과값이 결정되기 때문입니다. 단, 예외적으로 IO 작업과 같이 사용자 입력 시점에 따라 결과값이 결정되는 경우에 한해서는 실행 순서를 명시적으로 지정해줘야 합니다. 이렇게 실행 순서를 명시해야 하는 경우에 사용하는 것이 'do 표기법' 입니다.

즉, IO 작업같은 실행 순서를 지정해야 하는 함수는 처음에 do 로 시작해야 합니다. 이렇게 하면 일반적인 명령형 프로그래밍처럼 코딩 순서에 따라 위에서 아래로 함수가 순차적으로 실행되는 것이죠...
그런데 do 를 사용하지 않고도 실행 순서를 지정할 수 있는 방법이 있는데 모나드 클래스에 있는 (>>=) 과 (>>) 메소드를 사용하면 됩니다.
>>= 와 >> 는 왼쪽에 있는 함수의 결과값을 오른쪽에 있는 함수의 파라미터로 전달할 것인가 말 것인가의 차이를 갖고 있습니다.
예를 들어 simpleClient2' 함수를 다시 보면...

 

  1. simpleClient2' = do
        sock <- connectTo "localhost" (PortNumber 10000)
        writeLoop sock
        hClose sock
        putStrLn "disconnected"


connectTo 함수 수행 후 그 다음에 writeLoop가 실행되며 이 때 connectTo 함수의 반환값을 writeLoop 가 파라미터로 받으므로 위 소스는 아래와 같이 바꿀 수 있습니다.

  1. connectTo "localhost" (PortNumber 10000) >>= writeLoop

 

또 한 writeLoop 함수의 종료 부분인 6) 부분을 return () 가 아닌 return h 로 수정하면 역시 writeLoop의 반환값인 소켓 핸들을 hClose가 받아서 처리할 수 있으므로 연속해서 >>= 를 적용할 수 있습니다. 그리고 hClose는 반환값이 없고 putStrLn "disconnected" 역시 추가적으로 받아야할 파라미터가 없으므로 여기에는 >> 를 사용하여 순서만 지정할 수 있습니다.
결국 최종적으로 모나드 메소드를 이용하면 simpleClient2' 함수는 아래와 같이 바꿀 수 있습니다.

  1. moreSimpleClient2' = connectTo "localhost" (PortNumber 10000) >>= writeLoop >>= hClose >> putStrLn "disconnected"

    writeLoop h = do
        contents <- getLine
        hPutStrLn h contents
        hFlush h
        if (contents /= "bye")
            then writeLoop h
            else return h -- return () 대신 h 을 반환하도록 수정

 

 이렇게 모나드 함수를 사용하면 불필요한 파라미터나 do 구문을 없앨 수 있어 소스가 간단해지는 장점이 있지만 남용할 경우 가독성이 떨어지는 단점도 있습니다. 따라서 모나드 사용에 익숙하지 않다면 그냥 do 구문을 사용하는 것이 좋습니다.

다음 글에서는 쓰레드를 이용해서 여러 클라이언트의 접속을 허용하는 서버를 만들어 보겠습니다.

 

다중 접속 서버 프로그래밍

이번에는 쓰레드를 이용한 다중 접속 서버를 만들어 보겠습니다.
쓰레드를 생성하는 방법은 매우 간단한데 Control.Concurrent 모듈내에 정의된 forkIO 라는 함수를 사용하면 됩니다. forkIO 함수는 스레드 작업을 수행할 IO 타입 함수를 파라미터로 받고 생성된 ThreadId 를 반환합니다. 아래는 간단한 쓰레드 예제입니다.

 

  1. import Control.Concurrent

    main = do
        forkIO printLoop 'a'    -- 1)
        printLoop 'b'             -- 2)

    printLoop c = putChar c >> printLoop c -- C로 표현하자면 printLoop(char c) { for(;;) putchar(c); } 와 같음

 

forkIO 함수에 의해 1)에 있는 printLoop 함수는 별도의 쓰레드로 실행됩니다. 따라서 1)과 2)의 printLoop 함수는 병렬적으로 수행되어 'a'와 'b' 가 화면에 번갈아가며 무한 출력됩니다.

forkIO 를 이용해서 생성된 쓰레드는 OS 수준에서 생성되는 쓰레드보다 경량(lightweight)의 쓰레드입니다. 이것은 하스켈 프로그램을 실행시키는 런타임 시스템이 자체적으로 생성한 쓰레드이며 따라서 일반 쓰레드보다 생성이나 전환에 필요한 오버헤드가 적습니다. 즉, OS 수준에서 볼 때는 main은 하나의 쓰레드이며 이 main 쓰레드 자체에서 두 함수를 적절하게 스위칭해주는 것입니다.
만약 OS 수준의 쓰레드를 생성하려면 forkIO 대신에 forkOS 라는 함수를 사용합니다. forkOS는 내부적으로 pthread_create() 나 CreateThread()와 같은 시스템 API 를 호출하여 쓰레드를 생성합니다.
따 라서 forkIO와 forkOS가 생성한 쓰레드는 몇 가지 차이점이 있는데 결정적으로 쓰레드 지역 공간(TLS:Thread Local Strorage)를 갖느냐 그렇지 않느냐의 차이가 있습니다.(TLS에 대한 보다 자세한 설명은 위키피이아의 TLS설명부분이나 디버그랩의 윈도우즈 TLS 설명 글을 참고하세요.) 때문에 이런 TLS 특성을 이용하는 외부 라이브러리를 사용한다면 forkIO가 아닌 forkOS를 사용해야 합니다. 물론 그 외의 경우에는 대부분 forkIO를 사용하는 것이 더 효율적입니다.
그 외에 쓰레드에 대한 자세한 설명은 다음에 기회가 되면 하도록 하고 다중 접속 서버를 만들어 보도록 하겠습니다.

 

  1. import Network
    import IO
    import Control.Concurrent

    MultiServer = withSocketsDo MultiServer'
    MultiServer' = listenOn (PortNumber 10000) >>= (connLoop 1)    -- 1)
  2. connLoop n sock = do
        (h,host,port) <- accept sock
        let clientName = (show n) ++ "th client"
        putStrLn (clientName ++ " is connected")
        forkIO $ catch (readLoop h clientName) print        -- 2)
        connLoop (n+1) sock

  3. readLoop h name = do
        contents <- hGetLine h
        putStrLn (name ++ " says: " ++ contents)
        if (contents /= "bye")
            then readLoop h name
            else return ()

 

1) 은 이전 글에서 언급했듯이 모나드를 이용한 구문입니다. connLoop 함수는 각각의 접속자를 구분하기 위한 id 값과 - listenOn 함수가 반환하는 - 소켓 핸들을 파라미터로 받습니다. 여기서 소켓 핸들을 모나드를 통해 전달하는 것입니다.
여 기서 하스켈의 특징이 하나 나타납니다. connLoop 함수는 파라미터를 두 개 받는 함수인데 모나드는 하나만 전달이 가능하므로 원래는 모나드를 통해 값을 전달하는 것이 불가능합니다. 하지만 connLoop가 첫번째 파라미터를 받은 상태인 (connLoop 1) 이라는 형태로 변형되면 소켓 핸들 하나만 받을 수 있는 함수로 바뀌므로 모나드를 적용할 수 있습니다.(이 부분이 잘 이해가 가지 않으면 make it functional의 하스켈 함수 설명 부분을 참고하세요.)
첨 언하자면, 하스켈의 파라미터 처리 방식은 C,C++,Java 같은 명령형 프로그래밍 언어와 다릅니다. 명령형 언어는 파라미터 여러 개의 하나의 묶음으로 받아 동시에 처리하지만 하스켈은 파라미터를 왼쪽부터 순서대로 하나씩 받아 새로운 함수를 반환하는 형태로 동작합니다.
예를 들어 명령형 언어에서는 foo(x, y, z) 라는 함수가 있으면 foo 함수 호출 시 (x,y,z) 를 한꺼번에 foo 함수에 넘겨서 처리하지만, 하스켈에서 foo x y z 라고 하면 아래와 같이 처리됩니다.

 

(((foo x) y) z)

 

이 런 특성을 갖는 함수를 '커리(curry) 함수'라고 합니다. 하스켈은 기본적으로 커리 함수로 동작합니다. 기회가 되면 계속 예를 들겠지만 이런 커리 함수 특성은 - 역시 나중에 소개가 되겠지만 - '고차함수', 함수 합성 등과 함께 재사용을 극대화해주는 하스켈의 특징입니다.

2) readLoop 함수를 별도의 쓰레드에서 처리하도록 해줍니다. 바로 다음 줄에서 connLoop 를 재귀적으로 호출하기 때문에 어떤 클라이언트가 접속하면 해당 클라이언트의 데이터를 처리하는 readLoop 함수가 쓰레드로 실행되면서 입력값을 처리하는 동시에 다른 클라이언트의 접속을 처리할 수 있습니다.

다음 번에는 쓰레드 동기화 기능을 포함한 간단한 채팅 서버를 만들어 보겠습니다.

 

채팅 클라이언트 / 서버 프로그래밍

먼저 채팅 클라이언트 프로그램 소스는 아래와 같습니다.

 

  1. import Network
    import IO
    import Control.Concurrent

    chatClient = withSocketsDo chatClient'

    chatClient' = do
        h <- connectTo "localhost" (PortNumber 10000)
        forkIO $ listenTalk h
        writeLoop h
        hClose h
        putStrLn "disconnected"
  2. listenTalk h = hGetLine h >>= putStrLn >> listenTalk h

    writeLoop h = do
        contents <- getLine
        hPutStrLn h contents
        hFlush h
        if (contents /= "bye")
            then writeLoop h
            else return h

 

 이 프로그램은 크게 두 개의 쓰레드가 동작하는데 각각 writeLoop 와 listenTalk 함수를 수행합니다. 이미 기존에 언급한 내용에서 크게 벗어나지 않으므로 세부적인 설명은 생략하겠습니다.

다음은 채팅 서버 소스입니다.(이 소스는 안기영님의 하스켈 서버 프로그래밍 자료를 참고하였습니다.)

 

  1. import Network
    import IO
    import Control.Concurrent
    import Control.Exception hiding (catch)
    import List

    chatServer = withSocketsDo chatServer'
    chatServer' = do
        mvH <- newMVar []    -- 1)
        mvM <- newMVar []
        mvS <- newEmptyMVar    -- 2)
        let mvar = (mvH,mvM,mvS)
        forkIO $ catch (broadcast mvar) print
        listenOn (PortNumber 10000) >>= (chatLoop mvar)
  2. chatLoop mvar@(mvH,mvM,mvS) sock = do
        con@(h,host,port) <- accept sock
        putStrLn ("connected from " ++ host ++ ":" ++ (show port))
        modifyMVar_ mvH (return . (h:))    -- 3)
        forkIO $ catch (talk mvar con `finally` closeCon h mvH) print    -- 4)
        chatLoop mvar sock
  3. talk mvar@(_,mvM,mvS) con@(h,host,port) = do
        s <- hGetLine h
        modifyMVar_ mvM (return . ((show(host,port) ++ "says:" ++ s):))    -- 5)
        tryPutMVar mvS ()    -- 6)
        talk mvar con

  4. closeCon h mvH = do
        modifyMVar_ mvH (return . delete h)    -- 7)
        hClose h

  5. broadcast mvar@(mvH,mvM,mvS) = do
        takeMVar mvS        -- 8)
        msgList <- swapMVar mvM []    -- 9)
        hList <- readMVar mvH
        mapM_ (\h -> (mapM_ (safecast h) msgList)) hList    -- 10)
        broadcast mvar
        where safecast h msg = catch (hPutStrLn h msg >> hFlush h) print

 

 먼저 각각의 함수들에 대해 간략한 설명을 하자면,
chatServer' - 기본적인 소켓 및 동기화 리소스를 생성하는 역할 수행
chatLoop - 클라이언트가 접속을 시도하면 접속을 허용, 소켓 핸들을 리스트에 저장하고 해당 클라이언트로부터 수신되는 데이터를 감시할 쓰레드(talk 함수) 생성
talk - 클라이언트로부터 수신된 데이터를 메시지 리스트에 저장하고 수신된 사실을 알림
closeCon - 접속 해제 시 해당 소켓 핸들을 핸들 리스트에서 삭제
broadcast - 클라이언트로부터 수신된 데이터를 전체 클라이언트에게 송신

세부적인 사항을 설명하자면,
1) 쓰레드 동기화를 위한 객체들을 생성합니다. 채팅 서버에서는 아래와 같은 두 가지 경우에 동기화가 필요합니다. 
    첫째, 클라이언트 접속 시 생성된 소켓 리스트를 관리하는 리스트에 소켓 핸들을 추가/삭제할 때
    둘째, 클라이언트로부터 수신된 데이터를 관리하는 메시지 리스트에 메시지를 추가/삭제할 때 
이를 위해 여기서는 mvH(소켓 핸들 리스트를 관리) 와 mvM(메시지 리스트를 관리) 이라는 동기화 객체를 생성합니다.(MVar 동기화 객체에 대한 내용은 멀티쓰레드 동기화 프로그래밍 페이지를 참조하세요.)

2) 수신 메시지를 감시하는 쓰레드(talk 함수)들과 이 메시리들을 전체에 송신하는 쓰레드(broadcast 함수)는 별도로 동작하기 때문에 수신 이벤트 발생 시 이것을 broadcast 함수에게 알려주기 위한 수단이 필요한데 이 용도로도 MVar 객체를 이용할 수 있습니다. 
newEmptyMVar 함수는 newMVar 함수와 비슷하게 MVar 객체를 생성하지만 초기값을 지정하지 않고 '텅빈' 상태의 MVar 객체를 생성한다는 차이점이 있습니다.
이제 talk 쓰레드에서 이벤트를 수신하면 mvS 객체를 '꽉찬' 상태로 바꾸고 broadcast 쓰레드에서는 mvS 객체를 감시하고 있다가 '꽉찬'상태가 됐을 때만 저장된 메시지들을 전체 클라이언트에게 송신합니다.

3) 클라이언트가 접속되면 accept 함수에서 반환된 소켓 핸들을 mvH 객체에 저장된 핸들 리스트에 추가합니다. 이를 위해 modifyMVar_ 함수를 이용합니다. modifyMVar_ 함수는 mvH 객체에서 핸들 리스트를 꺼내 이것을 (return . (h:)) 함수의 파라미터로 넘깁니다. 즉 modifyMVar_ mvH (return . (h:)) 는 아래와 같은 동작을 수행합니다.

 

  1. hList <- takeMVar mvH
    newHList <- return (h:hList)
    putMVar mvH newHList

 

4) 데이터 수신을 위한 talk 쓰레드를 생성합니다. `finally` 함수는 자바의 finally 와 유사하게 talk 쓰레드가 - 정상적이건 비정상적이건 - 종료될 때 항상 수행할 함수를 명시하는 역할을 수행합니다. 여기서는 접속이 종료된 핸들을 처리하기 위해 closeCon 함수를 수행합니다.

5) 클라이언트로부터 메시지가 수신되면 해당 메시지를 mvM 객체가 관리하는 메시지 리스트에 추가합니다.

6) 데이터 수신 이벤트를 broadcast 함수에게 알리기 위해 mvS 객체의 상태를 '꽉찬' 상태로 변경합니다. 이 때 talk 쓰레드는 여러 개가 생성되며 따라서 동시에 mvS 객체를 '꽉찬' 상태로 바꾸려고 접근할 수 있습니다. 
어떤 쓰레드든지 한번만 수신 이벤트를 알리면 broadcast 함수는 동작할 것이므로 여러 쓰레드가 모두 mvS 객체 상태를 바꿀 필요는 없습니다. 왜냐하면 여러 메시지를 처리하기 위한 동기화는 이미 mvM 객체에서 수행하기 때문입니다. 그러므로 여기서는 putMVar 가 아닌 tryPutMVar 함수를 사용했습니다. 만약 다른 쓰레드에서 이미 mvS 를 '꽉찬' 상태로 바꾸었고 아직 broadcast 쓰레드가 동작하지 않았다면 tryPutMVar 호출은 그냥 무시될 것입니다.

7) 클라이언트의 접속이 종료되면 talk 쓰레드의 hGetLine 함수는 해당 핸들을 더 이상 참조할 수 없기 때문에 예외를 발생시킵니다. 이 때 4) 에서 언급한 finally 함수에 의해 closeCon 함수가 호출됩니다. closeCon 함수는 해당 핸들을 mvH 가 관리하는 핸들 리스트에서 삭제합니다. delete 함수는 List 모듈에 있는 함수로 리스트에서 특정 값을 삭제하는 기능을 수행합니다.

8) 수신 알림 이벤트가 발생하면 mvS 객체는 '꽉찬'상태가 되고 그래서 takeMVar mvS 함수는 블럭에서 해제됩니다.

9) mvM 객체가 저장하고 있는 메시지 리스트를 꺼내고 대신 빈 리스트를 넣습니다. 이렇게 해야 mvM 객체는 항상 아직 클라이언트에게 송신하지 않은 메시지들만 관리하게 됩니다. 그리고 전체 클라이언트에게 메시지들을 송신하기 위해 mvH 객체가 관리하는 소켓 핸들 리스트를 읽습니다. 이전 글에서 설명했듯이 readMVar 함수는 해당 객체가 관리하는 데이터를 꺼내 그 복사본을 반환하고 원본은 다시 MVar 객체 안에 저장합니다.

10) 메시지 리스트의 전체 메시지를 전체 핸들별로 차례대로 송신합니다. mapM_ 함수는 map 함수와 동일한 동작을 수행하는데 입/출력 작업을 수행하는 함수를 파라미터로 받을 수 있습니다. (map 함수는 입/출력 함수는 사용할 수 없습니다.)  
mapM_ (\h -> (mapM_ (safecast h) msgList)) hList 이것을 명령형 프로그래밍으로 표현하면 아래와 같습니다.

 

  1. for hList.first -> hList.last
        for msgList.first -> msgList.last
            sendmessage(h, msg)

 

 이렇게 해서 간단한 채팅 프로그램을 만들어 봤습니다. 채팅 프로그래밍에서 보셨듯이 MVar 는 데이터를 관리할 수 있는 동기화 객체라는 특성을 가지고 있어서 꽤 유용하며 복잡한 동기화 문제를 비교적 쉽고 다양하게 접근할 수 있는 유연성을 가지고 있습니다.
 혹시 이런 생각을 하신 분이 계실지 모르겠습니다. "어라? 하스켈에서는 변수가 없다고 하더니 MVar 객체에 데이터 집어넣고 빼고 수정하고 이런 거 보면 이거 변수 같은데?'
예... 맞습니다. MVar 객체는 데이터를 저장하고 상태가 바뀐다는 점에서 명령형 프로그래밍에서 사용하는 변수와 거의 차이가 없습니다. 하스켈에는 이런 특성을 지닌 몇 가지 객체들이 있는데 모두 모나드와 깊은 연관이 있습니다. 뭐 이런 건 자세히 파고들면 골치 아파지니 앞으로 천천히 알아보도록 하죠...

History

Last edited on 01/04/2008 17:57 by gimmesilver

Comments (0)

You must log in to leave a comment. Please sign in.