실질객관 bLOG :: 실질객관 bLOG
IT이야기/컴퓨터 일반

원격 데스크톱 연결 시 인증 오류 해결 방법


윈도우 업데이트 이후 원격 데스크톱(mstsc) 연결 시도 시,


아래 1번 혹은 2번과 같은 오류 메시지를 맞닥뜨리게 되는 경우가 있습니다.


(참고: 글쓴이의 서버OS는 Windows 2012 R2, 클라이언트OS는 Windows 10입니다)


윈도우 Home Edition의 경우 로컬 그룹 정책 편집기(이하 gpedit)가 설치되어 있지 않습니다.

gpedit 설치 방법에 대해 잘 정리된 블로그가 있어 주소를 공유하니 필요 시 참고하시면 되겠습니다.

http://prolite.tistory.com/743



#1




#2





침착하게 아래와 같이 대응합니다.





1. 시스템의 로컬 그룹 정책 편집기를 엽니다.


(윈도우) + R키로 실행을 누른 뒤 gpedit.msc 입력으로 실행



2. 아래 경로를 찾아 들어갑니다.


[로컬 컴퓨터 정책] - [컴퓨터 구성] - [관리 템플릿] - [시스템] - [자격 증명 위임]



3. Encryption Oracle Remediation 항목을 찾습니다.



4. 해당 항목을 더블클릭 한 뒤, 설정을 아래 대로 바꿔줍니다.


해당 기능을 사용하는데 보호 정도를 "취약함(Vulnerable)"으로 설정합니다. (...)





5. 적용을 누른 뒤 로컬 그룹 정책 편집기를 닫고, 다시 한번 원격 데스크톱 연결을 시도해봅니다.



6. 잘 되는 경우, 잘 된다는 댓글을 남깁니다. (소곤소곤)



안 되는 경우, 본문과 증상이 동일한지 확인 뒤, 동일하다면 서버와 클라이언트의 OS를 댓글로 남깁니다. (소곤소곤)






취약해지더라도(?) 일단 원격 데스크톱을 쓰기는 해야겠다 하시는 분들께


도움이 되길 바라면서 글을 줄입니다.


뜬금없이 연결 안 돼서 깜짝 놀랐네요.


업데이트 내용에 나와있고 메시지 창에 링크까지 나와있긴 하지만서도.. 어휴(...)


한숨 돌렸습니다.


모두 안전하고 행복한 날 되세요 :)

'IT이야기 > 컴퓨터 일반' 카테고리의 다른 글

원격 데스크톱 연결 시 인증 오류 해결 방법  (21) 2018.05.09
CISC와 RISC  (0) 2017.01.19
  1. 123 수정/삭제 답글

    감사합니다 소곤소곤

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      같은 현상을 겪으신 분께 도움이 되어 기쁘네요 (소곤소곤)

  2. ms무식자 수정/삭제 답글

    원격 데스크탑 연결 해결 됐습니다.

    감사합니다.

  3. socool 수정/삭제 답글

    windows 10 home 버전에서는 gpedit.msc가 실행이 안되네요.

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      home 에디션은 gpedit을 설치하셔야 합니다. 본문에 관련 정보가 있는 블로그 링크를 올려드릴게요.

  4. Favicon of http://nothing 윈도우를 싫어하는 1인 수정/삭제 답글

    좋은 정보 감사합니다.
    며칠전 아무생각없이 윈도우 업뎃을 하고는 업무상 데스크탑 연결을 했는데, 안되서 1시간 동안 삽질을 하다가 주인장님 덕분에 해결했습니다.

  5. backup man 수정/삭제 답글

    잘됩니다 감사합니다.

  6. 이것때문에 고민하던 1인 수정/삭제 답글

    잘 되네요 감사합니다.

  7. 여전히 안되는 1인 수정/삭제 답글

    gpedit 설치하고 위의 절차 따라서 해도 전 안되네요...ㅠㅠ
    윈도우7이구요. 서버는 2012R2인데...
    다른 분들은 다들 위 방법으로 다 되신다는데 전 안되네요 ㅠㅠ

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      동일한 오류메시지가 발생하는 경우인가요??

  8. 감사합니다 수정/삭제 답글

    해결하였습니다 감사합니다~

  9. 콜리 수정/삭제 답글

    와 잘 되네요~ 감사합니다 소근소근!

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      댓글 감사해요 소근소근

  10. 해결된1인 수정/삭제 답글

    알려주신대로 제 PC를 설정 변경했더니 해결되었습니다.
    감사합니다~(소곤소곤)

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      도움되어 다행입니다!! 잘 되던게 안 되면 속상하니까요 (소곤소곤)

  11. 베니 수정/삭제 답글

    클라이언트 windows 10 home
    서버 windows server 2012r2

    위와같이 그룹정책 변경하고 했는데 안됩니다. 재부팅하고도 안됩니다. 업데이트 안된 노트북으로 연결하니 됩니다.
    업댓된 컴은 안됩니다. 무엇을 더 시도해 볼까요.

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      어떤 오류메시지가 발생하는지 알려주시면 시간날때 찾아보겠습니다.

  12. 감사합니다~ 수정/삭제 답글

    감사하면 댓글을 달아야지요~ 감사합니다. (소곤소곤)

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      어휴 뭘 이런걸 다.. (소곤소곤) 감사합니다.

  13. 지평 수정/삭제 답글

    위와 같이 했는데 메세지 내용만 바뀌고 안되네요.

    OS : win7 por K
    메세지 내용 : 인증 오류가 발생했습니다.(코드 : 0x80004005).

    도움 부탁드립니다. (소곤소곤)


    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      제어판 - 관리 도구 - 로컬 보안 정책 실행하신 뒤,
      보안 설정 - 로컬 정책 - 보안 옵션에 들어가셔서, '네트워크 보안: LAN Manager 인증 수준' 항목을 더블 클릭하시고, 드롭다운 박스의 화살표를 누르신 다음 'LM 및 NTLM 보내기 - 협상되면 NTLMv2 세션 보안 사용(& )' 항목을 선택하신 다음 적용 - 확인 순으로 누르세요. 윈도우 다시 시작하신 다음에 하시려던 작업 해보시고 댓글 주세요. (소곤소곤에만 반응하는 걸 들키다니 부들부들..)

자작프로그램/AHK-PC

팀뷰어 연결 해제 시의 후원 세션 팝업 신속하게 닫기- 킬팝업2


2015년판 대비 상당히 개선된 프로그램이 아래에 있습니다.


긴 글 내용을 원치 않으시는 분들께서는 모쪼록 아래로 쭉 내리시면 되겠습니다.





수 년 전에 안드로이드 에뮬레이터 + 수제 매크로 스크립트를 이용해서 게임을 돌렸었습니다.


화면을 대상으로 한 이미지 또는 픽셀 색상 검출 함수를 즐겨 쓰던 시절이었기에,


가끔 팝업이라도 뜨면 게임 매크로가 멈추는 참사아닌 참사가 발생하곤 했는데...


상당한 빈도로 이런 현상을 일으키던 주범이 아래 팀뷰어 후원 세션 팝업이었습니다.







잘 돌아가고 있는지, 새롭게 득템한 것은 없는지를 거의 실시간으로 (..그럴거면 도대체 매크로는 왜...) 확인하다보니,


가끔 접속시간이 길어지는 경우도 생기고, 그 경우, 연결을 종료하면 위와 같은 팝업이 뙇.......


그래서 저거 자동으로 끄는 스크립트를 하나 짜놨었더랍니다.


이전 글 보러 가기 - [자작프로그램/AHK-PC] - 팀뷰어 연결해제 시의 팝업 자동으로 닫기





그리고 잊고 살았습니다.


3년여 시간이 흐르고, 어느 날 블로그 방문 로그를 보는데, 생각보다 킬팝업을 찾으시는 분들이 많더라는 겁니다.


중요한 것은, 황공하게도 제 블로그 출처를 남겨주시면서 가져가주신 분들이 유독 많으시더라는 겁니다.


그래서 준비해봤습니다.



Kill Popup II.exe




마땅히 떠오르는 이름이 없어서 진부하게 그냥 킬팝업 2라고 이름 붙였습니다.


형만한 아우가 없다는데, 과연 그럴까요?


편의상 1탄, 2탄으로 지칭하겠습니다.


1탄의 경우, 굉장히 원시적인 로직으로 짜져있었습니다.


그래서 CPU 점유율이... 상당했죠(...) 그럼에도 불구하고 많은 사랑을 받다니.. 참 세상 일은 알다가도 모를 일입니다.


2탄의 경우, 반응 속도는 개선되었고, CPU 점유율은 대폭 감소시켰습니다.




별도의 명시가 없는 한, 제 블로그에 제가 게시하는 모든 프로그램이나 스크립트는 출처없는 자유로운 이용이 가능하십니다.


하지만 이게 뭐라고 번거롭게 출처 남겨가면서 소개해주신 분들... 감사합니다.


정말 별 것 아님에도 불구하고 다른 사이트에서 제 블로그 주소와 칭찬 글을 발견하게 되면 마음이......!


아마 블로그나 카페 하시는 분들 다들 그런 찡한 마음에 컨텐츠를 생산하시는 게 아닌가 싶습니다 :)




여튼, 킬팝업을 아직 사용하고 계시는 분들이 있으실지는 모르겠지만,


그래도.. 1탄 사용하시던 분들이 계신다면 2탄을 사용해보시길 바라는 취지로 간만에 업데이트를 남기고 이만 물러가겠습니다.


항상 안전하고 행복한 나날 되시길 바랍니다.




뱀발1.

 비번이나 메일 주소 남기라는 등의 번거로운 과정은 생략하고, 필요하신 분 혹여라도 있으시면 아래 소스 참고하세용.


Kill Popup II.ahk


  1. 감쟈 수정/삭제 답글

    고맙습니다 :) 즐거운 휴일 보내세요!

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      감쟈님도 안전하고 좋은 나날 되세요 :)

  2. 스르륵 수정/삭제 답글

    킬팝업1 정말 잘쓰고 있었는데 안그래도 cpu 점유율을 너무 많이 잡아먹어서 고민이 많았습니다 ㅠㅠ 그렇다고 대체제가 있는 것도 아니기에 cpu를 교체할까 고민하던 중에
    킬팝업2가 나왔네요 ㅎㅎ 감사히 잘쓰겠습니다.^^

IT이야기/Java

네티 4.x 유저 가이드(Netty Uesr guide for 4.x)





Netty User guide for 4.x를 옮기며...
[최종 수정 일자 : 2018년 2월 26일]

문서 볼륨이 크지 않고, 이해가 필수적인 부분에 대해 굉장히 잘 설명되어 있어서 옮겨봅니다.

오역은 없을 것이라 생각됩니다만 원문과 비교하면서 보시길 권장드리며,

혹여나 잘못된 부분이 있을 경우, 댓글로 따끔하 게 알려주시면 확인하는대로 수정해놓도록 하겠습니다.

혹시 필요하신 분이 계시면 출처 표기 없이 자유롭게 가져가시거나, 수정하시거나 하셔도 관계 없습니다.



시작합니다.

Netty 4.x 버전 사용자를 위한 안내서



서문

개요

 상호간 통신을 위해서는 범용 어플리케이션이나 라이브러리가 흔히 사용됩니다. 웹서버로부터 필요한 정보를 얻어오거나, 웹 서비스의 원격 프로시저 호출을 위해 HTTP 클라이언트 라이브러리를 사용하는 것처럼요. 하지만 범용 프로토콜이나 이를 차용한 라이브러리를 사용하는 것이 조금 부적합한 경우가 존재합니다. 대용량 파일, 이메일을 주고 받는 용도로 범용 HTTP 서버를 사용하지 않습니다. 금융 정보나 다중접속 게임의 데이터처럼 준실시간 통신이 필요한 경우에도 보통은 범용 HTTP 서버 어플리케이션을 사용하지는 않는다는 것이죠.
 즉, 아주 제한적인 목적을 달성하기 위해, 고도로 최적화된 프로토콜이 필요한 경우가 생각보다 많다는 겁니다.
  AJAX 기반의 채팅 어플리케이션이나, 미디어 스트리밍, 혹은 대용량 파일 전송에 최적화된 HTTP 서버 프로토콜 구현이 필요한 경우를 생각해보시기 바랍니다. 개발자의 필요에 따라 완전히 새로운 프로토콜을 설계하거나, 새로운 프로토콜을 사용해야 하는 경우도 얼마든지 있을 수 있죠. 경우에 따라서는 기존에 존재하는 구형 시스템과의 호환성을 확보해야 할 수도 있는데 이런 경우, 개발자들은 안정성과 성능을 저하시키지 않고 얼마나 신속하게 개발을 완료하는지가 중요한 변수가 되기 십상입니다.
  

해결책

 Netty 프로젝트는 유지보수와 규모조정이 용이한 프로토콜 서버 및 클라이언트의 신속한 개발을 목적으로 개발된 이벤트-드리븐 방식의 네트워크 어플리케이션 프레임워크를 제공하고자 하는 노력의 일환으로 시작되었습니다.
  즉, Netty는 프로토콜 서버나 클라이언트와 같은 네트워크 어플리케이션의 쉽고 빠른 개발을 가능하게 하는 NIO 클라이언트 서버 프레임워크라고 할 수 있겠습니다. TCP나 UDP 소켓 서버 개발 등의 네트워크 프로그래밍 과정을 실로 엄청나게 간소화해줍니다.

  '쉽고 빠르다'지만 이로 인한 결과물의 유지보수가 고통스럽거나, 성능 문제가 있어서는 안 됩니다. Netty는 FTP, SMTP, HTTP 뿐 아니라 다양한 binary 및 text 기반 기존 프로토콜들에 대한 고찰과 경험을 바탕으로, 꼼꼼하게 설계되었습니다.
  그 결과로써 Netty는 성능이나 유지보수성에 대한 부정적인 영향 없이, 쉬운 개발과, 성능, 안정성, 그리고 유연성까지를 모두 확보하는데 성공하였습니다.
  
 동일한 장점들을 가지고 있다고 주장하는 다른 네트워크 어플리케이션 프레임워크를 사용 중인 개발자들도 분명히 많을지언데, 이 분들은 아마 Netty만의 장점이 도대체 무엇이냐라는 의문을 가질 겁니다. 이에 대한 답변은 프레임워크의 방향성이라고 할 수 있겠습니다. Netty는 애당초 개발자들에게 API와 구현 자체를 가장 편하게 수행하기 위한 목적으로 개발되었습니다. 눈으로 확인할 수 있는 장점은 아니지만, 이 안내문을 읽으면서, 또 Netty를 이용한 개발을 수행하면서 개발자가 이 '방향성'을 직접 느껴보실 수 있을 것입니다.

시작하기

 본문은 개발자가 빠르게 Netty에 익숙해질 수 있도록 간단한 예제들을 통해 Netty의 핵심적인 구조를 소개하고 있습니다. 본문의 끝에 다다르는 시점에, 이 글의 독자는 Netty 기반의 서버와 클라이언트 프로그램을 작성할 수 있을 것입니다. 핵심적인 내용부터 보길 원한다면 "2장, 설계 구조"를 먼저 보고 난 뒤, 다시 이 지점으로 돌아오길 권합니다.

시작하기에 앞서

 본문에 소개되는 예제들을 구동하기 위해서는 다음과 같은 2가지의 최소 요구조건이 필요합니다: Netty의 최신 버전과, 1.6 또는 그 이상의 JDK입니다. 최신 버전의 Netty는 프로젝트 다운로드 페이지(http://netty.io/downloads.html)에서 받으실 수 있고, 올바른 버전의 JDK를 다운로드 받기 위해서는 원하는 JDK 공급자의 웹 사이트를 참조하시기 바랍니다.
 
  본문을 읽으면서 본문에 소개된 클래스에 대해 궁금한 부분이 생길 수도 있을 것입니다. 클래스에 대한 추가적인 정보가 필요한 경우, API 레퍼런스를 참조하기 바랍니다. 모든 클래스명은 레퍼런스에 명시되어 있으며, 편의를 위해 온라인 페이지에 대한 링크 형태로 제공됩니다. 잘못된 정보가 존재하는 경우 Netty project community(http://netty.io/community.html)에 알려주세요. 문법이나 오타, 혹은 문서의 품질 향상을 위한 제안 사항도 환영합니다.

Discard(폐기) 서버 작성하기

 세상에서 가장 간단한 프로토콜은 "Hello, World!가 아닌, DISCARD(폐기) 입니다. 이 프로토콜은 어떠한 응답도 제공하지 않고, 수신한 모든 데이터를 단순히 폐기하는 겁니다.
  이 DISCARD 프로토콜을 구현하기 위해 해야할 것이라고는 수신한 데이터를 무시하는 것밖에 없습니다. Netty가 발생시키는 I/O 이벤트를 처리하는 "핸들러"를 구현하는 것부터 바로 시작해보도록 하죠.

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package io.netty.example.discard;
     
    import io.netty.buffer.ByteBuf;
     
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.ChannelInboundHandlerAdapter;
     
    /**
     * Handles a server-side channel.
     */
    public class DiscardServerHandler extends ChannelInboundHandlerAdapter { // (1)
     
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) { // (2)
            // Discard the received data silently.
            ((ByteBuf) msg).release(); // (3)
        }
     
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
            // Close the connection when an exception is raised.
            cause.printStackTrace();
            ctx.close();
        }
    }
    cs

  •  1. DiscardServerHandler는 ChannelInboundHandlerAdapter를 상속합니다. ChannelInboundHandlerAdapter는 ChannelInboudHandler의 구현체인데, ChannelInboundHandler는 개발자가 override해서 사용할 수 있도록 다양한 이벤트 핸들러 메서드를 제공합니다. 우선 지금 당장은 인터페이스를 직접 구현하기보다 ChannelInboundHandlerAdapter 클래스를 상속받는 것이 편하겠네요.
  •  
  •  2. 여기서 channelRead() 메서드를 override합니다. 이 메서드는 클라이언트로부터 새로운 데이터를 받을 때마다, 즉, 새로운 메시지를 받을 때마다 호출됩니다. 이 예제에서 수신한 메시지의 타입은 ByteBuf입니다.
  •  
  •   3. DISCARD 프로토콜을 구현하기 위해, 핸들러는 수신한 메시지를 완전히 무시해야만 합니다. ByteBuf는 참조형 객체로, 반드시 명시적인 release() 메서드를 통해 해제가 되어 줘야만 합니다. 핸들러에게 전달된 참조형 객체의 해제는 핸들러의 책임이라는 것을 반드시 기억하시기 바랍니다. 대개, channelRead() 핸들러 메서드는 아래와 같이 구현됩니다.
  •   
  • 1
    2
    3
    4
    5
    6
    7
    8
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        try {
            // Do something with msg
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }
    cs

  •  4. exceptionCaught() 이벤트 핸들러는 (i) I/O 에러가 발생해서 Netty가 exception을 발생시킨 경우, 혹은, (ii) 핸들러 구현체에서 이벤트를 처리하는 도중에 exception이 발생한 경우, 관련 정보를 담고 있는 Throwable 객체와 함께 호출됩니다. 예외 상황에 대한 개발자의 개발 의도 등에 따라 connection을 닫기 전에 오류 코드에 대한 응답내용을 보내는 등의 변화는 있을 수 있겠지만 대체적으로 이 핸들러에서는 발생한 exception에 대한 로그를 기록하고, 사용된 채널도 닫아주게 됩니다.
  
 여기까지 잘 따라오셨습니다.  지금까지 DISCARD 서버의 앞쪽 반틈을 작성해봤습니다. 남은 것은 DiscardServerHandler를 탑재한 서버를 구동할 수 있는 main() 함수를 작성하는 것 뿐이네요.

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    package io.netty.example.discard;
     
    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.ChannelOption;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    import io.netty.channel.socket.nio.NioServerSocketChannel;
     
    /**
     * Discards any incoming data.
    */
    public class DiscardServer {
        
        private int port;
        
        public DiscardServer(int port) {
            this.port = port;
        }
        
        public void run() throws Exception {
            EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            try {
                ServerBootstrap b = new ServerBootstrap(); // (2)
                b.group(bossGroup, workerGroup)
                 .channel(NioServerSocketChannel.class// (3)
                 .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                     @Override
                     public void initChannel(SocketChannel ch) throws Exception {
                         ch.pipeline().addLast(new DiscardServerHandler());
                     }
                 })
                 .option(ChannelOption.SO_BACKLOG, 128)          // (5)
                 .childOption(ChannelOption.SO_KEEPALIVE, true); // (6)
        
                // Bind and start to accept incoming connections.
                ChannelFuture f = b.bind(port).sync(); // (7)
        
                // Wait until the server socket is closed.
                // In this example, this does not happen, but you can do that to gracefully
                // shut down your server.
                f.channel().closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
                bossGroup.shutdownGracefully();
            }
        }
        
        public static void main(String[] args) throws Exception {
            int port;
            if (args.length > 0) {
                port = Integer.parseInt(args[0]);
            } else {
                port = 8080;
            }
            new DiscardServer(port).run();
        }
    }
    cs

  •  1. NioEventLoopGroup은 I/O 동작을 처리하는 다중 스레드 이벤트 루프입니다. Netty는 여러 방식의 통신을 위해 다양한 EventLoopGroup 구현체를 제공합니다. 이 예제에서는 서버측 어플리케이션을 구현하는 것이 목적이기 때문에 2개의 NioEventLoopGroup을 사용하게 될 겁니다. 첫번째 그룹은 보통 'boss'라고 많이 불리는데, 이 boss는 들어오는 연결을 수락하고, 해당 연결을 두번째 그룹에 등록해주는 역할을 수행합니다. 두번째 그룹 'worker'는 boss가 등록해준 연결에 대해 발생하는 트래픽을 처리하죠. 얼마나 많은 스레드를 사용할 것인지와, 생성된 Channel에 어떤 식으로 맵핑할 것인지가 EventLoopGroup의 구현체에 따라 달라지되, 생성자를 통해 별도로 지정해줄 수 있습니다.
  •  
  •  2. ServerBootstrap은 서버를 구성할 수 있도록 해주는 도우미 클래스라고 보면 되겠습니다. 개발자는 Channel 클래스를 직접적으로 이용해서 서버를 구성할 수도 있겠지만 이는 굉장히 지루한 작업이며, 대부분의 경우, 이런 직접적인 작업이 요구되지 않음을 알아두면 좋을 듯 합니다.
  •  
  •  3. 들어오는 새로운 연결을 수락하고 새로운 Channel을 생성하기 위해 NioServerSocketChannel 클래스를 사용하겠다고 명시했습니다.
  •  
  •  4. 이 지점에 명시된 핸들러는 새로 연결이 수락되고 채널이 생성되면 해당 채널에 대해 수행됩니다. ChannelInitializer는 사용자가 새로운 Channel에 대한 설정을 쉽게 수행할 수 있도록 하기 위한 목적으로 제공되는 핸들러입니다. 개발된 네트워크 어플리케이션을 구현하기 위한 DiscardServerHandler와 같은 핸들러를, 새롭게 생성된 Channel의 ChannelPipeline에 설정하는 용도로 주로 사용됩니다.
  •  
  •  5. Channel 구현체에 특화된 매개변수를 설정할 수도 있습니다. TCP/IP 서버 프로그램 작성하고 있는만큼, tcpNoDelay나 keepAlive와 같은 소켓 옵션을 부여해줄 수 있는 것이죠. 지원되는 ChannelOption들을 확인하려면 ChannelOption 클래스나 ChannelConfig 구현체의 API 레퍼런스를 참고하시기 바랍니다.
  •  
  •  6. option()도 있고, childOption()도 있다는 것을 알아차리셨나요? option()은 들어오는 연결 수락을 위한 NioServerSocketChannel에 추가적인 설정을 추가하기 위해 사용되고, childOption()은 부모 ServerChannel(이 예제에서는 NioServerSocketChannel)이 수락한 연결로 인해 생성된 Channel에 추가적인 설정을 추가하기 위해 사용됩니다.
  •  
  •  7. 준비가 다 된 것 같습니다. 이제는 정말 port를 지정해주고, 서버를 구동하는 것만 남았네요. 여기서는 예제가 구동되는 로컬 시스템의 모든 NIC(네트워크 인터페이스 카드)의 8080번 포트를 지정했습니다. 지정 주소만 다르다면 bind() 메서드를 개발자가 원하는대로 얼마든지 호출할 수 있게 된 것이죠.
 
 축하합니다! Netty를 이용한 첫번째 서버 개발을 마치셨네요.
 

수신한 데이터 살펴보기

 첫번째 서버 프로그래밍을 마친 만큼, 이게 정말 동작하는지를 확인해볼 필요가 있겠죠. 가장 쉬운 방법은 telnet 명령어를 이용하는 방법입니다. 예로써, 명령 프롬프트에서 telnet localhost 8080 을 입력하여 접속한 뒤, 아무 문자나 타이핑을 해보는 거죠.
  하지만, 그것만으로 서버가 잘 동작하고 있다고 말할 수 있을까요?
  DISCARD(폐기) 서버니까 알 수 없죠. 그 어떤 응답도 돌려주질 않으니까요. 서버가 동작하는지를 확인해보기 위해 DiscardServerHandler 핸들러의 channelRead() 메서드에 몇 줄의 코드를 더 추가해보도록 합니다.
  
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf in = (ByteBuf) msg;
        try {
            while (in.isReadable()) { // (1)
                System.out.print((charin.readByte());
                System.out.flush();
            }
        } finally {
            ReferenceCountUtil.release(msg); // (2)
        }
    }
    cs

  •  1.이 비효율적인 루프는 사실상 System.out.println(in.toString(io.netty.util.CharsetUtil.US_ASCII) 로 대체될 수 있습니다.
  •  2. 이 라인 대신, in.release(); 를 넣어줄 수도 있겠습니다.

 지금 서버 쪽에서 telnet 명령을 실행 중이라면, 서버가 받은 데이터를 화면에 표시하고 있다는 것을 확인할 수 있을 겁니다.
 DISCARD 서버 예제의 full 소스 코드는 io.netty.example.discard 패키지에 포함되어 있습니다.

ECHO(에코) 서버 작성하기

 지금까지 서버는 아무런 응답 없이 수신한 데이터를 화면에 표시하기만 했습니다만, 보통 서버는 요청을 받으면 응답을 해야하기 마련이죠. ECHO 서버(받은 요청을 그대로 응답으로 돌려주는 서버, 일명 무지개반사 서버) 을 구현하면서 어떻게 클라이언트에게 응답 메시지를 줄 수 있는지를 배워보도록 하겠습니다.

 앞에서 구현해본 DISCARD(폐기) 서버와의 유일한 차이점은 받은 데이터를 화면에 표시하는게 아니라, 다시 돌려보낸다는데 있습니다. 한번 더 channelRead() 메서드를 수정해보도록 하죠.
 
  •  
    1
    2
    3
    4
    5
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg); // (1)
        ctx.flush(); // (2)
    }
    cs

  •  1. ChannelHandlerContext 객체는 각종 I/O 이벤트와 동작을 트리거할 수 있는 다양한 기능을 제공합니다. 여기서는 write(Object) 메서드를 이용해서 전달받은 메시지를 그대로 써주고 있는 겁니다. DISCARD 예제 때와는 다르게, 전달받은 메시지에 대한 해제를 수행하고 있지 않다는 것에 주목하시기 바랍니다. 전달받은 메시지에 대한 해제가 분명히 핸들러의 책임인 것은 틀림없지만, write의 경우, write가 완전히 완료된 이후에 Netty가 해당 객체에 대한 해제를 수행해주기 때문에 해제문이 없는 것입니다.
  •  
  •   2. ctx.write(Object)를 수행한 것으로 write의 완료가 이루어지지 않습니다. Object는 내부 버퍼에 저장될 뿐이며, ctx.flush()에 의해 실질적으로 상대 시스템으로 전송되게 되죠. 이 두 줄은 간단하게 ctx.writeAndFlush(msg)로 줄여 사용할 수 있습니다.
  
 telnet을 실행해서 테스트를 다시 해본다면, 개발자가 입력한 문구가 되돌아오고 있는 것을 확인할 수 있을 것입니다.
 ECHO 서버 예제의 full 소스 코드는 io.netty.example.echo 패키지에 포함되어 있습니다.

TIME(시간) 서버 작성하기

 이번에 구현해볼 내용은 TIME(시간) 프로토콜입니다. 이전의 예제들과 다른 점은, 이번 예제 서버는 아무런 요청을 받지 않은 상황에서, 32-bit 정수로 구성된 메시지를 작성하고, 발송하되, 메시지 송신이 완료되면 해당 연결을 종료한다는 점입니다. 이 예제에서 독자는 메시지를 어떻게 만들고, 발송하는지, 그리고 완료 시에 연결을 어떻게 종료하는지를 배우게 될 것입니다.
 
  연결이 일단 수립이 되자마자 메시지를 발송하되, 입력 데이터는 무시할 것이기 때문에 이번만큼은 channelRead() 메서드를 사용해서는 안 됩니다. 대신, 이번에는 channelActive() 메서드를 사용하게 될 것이며, 사용 예는 아래와 같습니다.
  
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package io.netty.example.time;
    public class TimeServerHandler extends ChannelInboundHandlerAdapter {
     
        @Override
        public void channelActive(final ChannelHandlerContext ctx) { // (1)
            final ByteBuf time = ctx.alloc().buffer(4); // (2)
            time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L));
            
            final ChannelFuture f = ctx.writeAndFlush(time); // (3)
            f.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    assert f == future;
                    ctx.close();
                }
            }); // (4)
        }
        
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
    cs

  •  1. 앞서 설명한 내용과 같이, channelActive() 메서드는 연결이 수립되고 트래픽을 발생시킬 수 있는 시점에 호출됩니다. 현 시각을 나타내는 32-bit 정수를 이 메서드 안에서 작성해보도록 합니다.
 
  •  2. 새로운 메시지를 작성하기 위해, 메시지가 담길 버퍼를 먼저 할당해주어야 합니다. 32-bit 정수를 내보낼 것이기 때문에 최소 4 byte 용량을 가지는 ByteBuf 객체를 생성하도록 합니다. 이 새로운 버퍼 생성을 위해 ChannelHandlerContext.alloc()을 통해 ByteBufAllocator 객체를 반환받은 뒤, 4 byte의 크기를 가지는 새로운 버퍼를 생성자를 통해 생성했습니다.
 
  •  3. 이제 생성된 메시지를 여느 때와 같이 write 해보도록 합니다.
  •  헌데, 잠깐 짚고 넘어가야할 부분이 있죠. flip은 왜 생략되었을까요? NIO로 메시지를 보낼 때에는 대개 java.nio.ByteBuffer.flip() 메서드를 사용했을 겁니다. ByteBuf는 해당 메서드를 가지고 있지 않은데, 이는 ByteBuf 객체가  읽기용과 쓰기용 각각, 2개의 포인터를 가지고 있기 때문입니다. ByteBuf에 뭔가를 기록하게 되면 writer index는 증가하지만 reader index는 변하지 않습니다. reader index와 writer index는 메시지의 시작과 끝을 알려주는 역할을 수행하게 되구요.

  • 한편, NIO 버퍼는 flip 메서드를 호출하지 않는다면 메시지 내용의 시작과 끝을 명확하게 알 수 있는 방법을 별도로 제공하지 않습니다. 그래서 buffer에 대한 flip 처리를 빠트리게 되면 아무것도 전송되지 않거나, 올바르지 않은 데이터가 전송되기 마련이었습니다. Netty에서는 이런 문제가 발생할 수가 없는데, 이는 서로 다른 동작 타입에 대한, 서로 다른 포인터를 Netty에서 제공하기 때문입니다. 익숙해지면 삶이 한결 편안해진 것을 체감할 수 있을 겁니다. -- flipping 없는 삶이란!ㅋㅋㅋㅋㅋㅋ

  • 또 한 가지, 기억해야 할 부분은, ChannelHandlerContext.write() (그리고 writeAndFlush()도..) 메서드는 ChannelFuture 객체를 반환한다는 점입니다. ChannelFuture는 아직 수행되지 않은 I/O 동작을 나타냅니다. Netty에서는 모든 동작이 비동기적이기 때문에 이미 요청이 완료된 동작들이 아직 수행되지 않았을 수 있다는 것입니다. 아래 코드의 예를 보면, 메시지가 발송되기 전에 연결이 닫힐 수도 있다는 겁니다.

  • 1
    2
    3
    Channel ch = ...;
    ch.writeAndFlush(message);
    ch.close();
    cs

  • 그렇기 때문에 개발자는 ChannelFuture가 완료되고 난 이후에 close() 메서드를 호출해줄 필요가 있습니다. write() 메서드의 경우, 동작이 모두 완료되면 ChannelFuture의 모든 리스너에 동작이 완료되었음을 알려주고, ChannelFuture 자체를 반환해줍니다. close() 메서드도 마찬가지입니다. 즉각적으로 연결을 종료하지는 않으며, 이 메서드 역시 결과값으로써 ChannelFuture를 반환합니다.

  •  4. 그렇다면 write 요청이 완료되었다는 것을 개발자가 어떻게 알 수 있을까요? 반환된 ChannelFuture에 ChannelFutureListener 한개를 추가함으로써 간단히 알 수 있게 됩니다. 위 예제코드에서 우리는 익명 ChannelFutureListener 클래스를 만들었고, 이를 통해 작업이 완료되면 연결을 닫도록 했죠.
  •  사전 정의된(pre-defined) 리스너를 통해 아래와 같이 간소화된 코드를 사용할 수도 있습니다.
  
  • 1
    f.addListener(ChannelFutureListener.CLOSE);
    cs

 작성된 TIME(시간) 서버가 잘 동작하는지를 확인하기 위해 아래와 같은 UNIX rdate 명령어를 이용해볼 수 있습니다.
 
  • 1
    $ rdate -<port> -<host>
    cs

TIME 클라이언트 작성하기

 DISCARD나 ECHO서버 때와는 달리, TIME 서버를 위해서는 클라이언트가 필요한데, 이는 사람이 직접 32-bit짜리 이진 데이터를 달력에 보이는 것과 같은 날짜 형태로 일일이 변환할 수는 없기 때문입니다. 이번 파트에서는 서버의 정상 동작을 어떻게 담보하는지, 그리고 Netty를 활용한 클라이언트를 어떻게 작성하는지를 배워볼 겁니다.

 Netty를 이용한 서버와 클라이언트 간의 가장 큰 차이점이자 유일한 차이점이라고 한다면 다른 Bootstrap과 Channel 구현체를 사용한다는 점이 되겠습니다. 다음 코드를 살펴보도록 하죠.
 
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    package io.netty.example.time;
    public class TimeClient {
        public static void main(String[] args) throws Exception {
            String host = args[0];
            int port = Integer.parseInt(args[1]);
            EventLoopGroup workerGroup = new NioEventLoopGroup();
            
            try {
                Bootstrap b = new Bootstrap(); // (1)
                b.group(workerGroup); // (2)
                b.channel(NioSocketChannel.class); // (3)
                b.option(ChannelOption.SO_KEEPALIVE, true); // (4)
                b.handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new TimeClientHandler());
                    }
                });
                
                // Start the client.
                ChannelFuture f = b.connect(host, port).sync(); // (5)
     
                // Wait until the connection is closed.
                f.channel().closeFuture().sync();
            } finally {
                workerGroup.shutdownGracefully();
            }
        }
    }
    cs

  • 1. Bootstrap은 ServerBootstrap과 유사하지만, 클라이언트측의 서버와 무관한 채널, 혹은 연결이 요구되지 않는 채널을 위한 객체라는 점이 다르다고 볼 수 있겠습니다.

  • 2. 하나의 EventLoopGroup을 명시할 경우, 이 그룹은 boss이자 worker 그룹으로써 동작합니다만, 클라이언트측에서 boss 그룹으로서의 역할은 요구되지 않습니다.

  • 3. 클라이언트측 채널을 생성하기 위해서 NioServerSocketChannel 대신 NioSocketChannel이 사용됩니다.

  • 4. ServerBootstrap에서 사용되었던 childOption()이 사용되지 않는다는 점을 기억해두도록 합니다. 클라이언트측 SocketChannel은 부모 객체가 존재하지 않기 때문입니다.

  • 5. bind() 메서드가 아닌, connect() 메서드를 호출해야 합니다.

 보시는 바와 같이, 서버측 코드와 크게 다르지 않습니다. 이제 ChannelHandler 구현체를 보도록 할까요? 이 핸들러는 서버로부터 32-bit의 정수를 전달 받아, 사람이 읽을 수 있는 형태의 데이터로 변환하고, 화면에 변환된 시간을 표시해준 다음, 연결을 닫는 역할을 수행해야 할 것입니다.
 
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package io.netty.example.time;
    import java.util.Date;
    public class TimeClientHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf m = (ByteBuf) msg; // (1)
            try {
                long currentTimeMillis = (m.readUnsignedInt() - 2208988800L) * 1000L;
                System.out.println(new Date(currentTimeMillis));
                ctx.close();
            } finally {
                m.release();
            }
        }
     
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
    cs

  • 1. TCP/IP 프로토콜을 이용할 때, Netty는 전달받은 데이터를 ByteBuf 객체를 통해 읽어들입니다.

 복잡해보이지도 않고, 서버측 소스와 크게 달라 보이는 것도 없습니다. 그런데, 이 핸들러에서는 간헐적으로 IndexOutOfBoundsException이 발생하지요. 왜 그런지 다음 파트에서 알아보도록 하겠습니다.
 

흐름 기반 통신 취급하기

Socket Buffer에 대해 짚고 넘어가야 할 점

 TCP/IP와 같은 흐름 기반 통신에서, 수신된 데이터는 Socket receive buffer에 저장되게 됩니다. 안타깝지만, 이 흐름 단위 통신용 버퍼는 패킷 단위 큐가 아닌, byte단위 큐라는 점입니다. 말인즉슨, 송신측에서 2개의 독립된 패킷으로, 2개의 메시지를 발송하더라도 운영체제에서는 이 데이터를 2개의 독립된 메시지가 아닌, 한 뭉치의 byte로 취급하게 된다는 겁니다. 이 때문에 수신 측에서 읽어들인 데이터가 송신측에서 발송한 데이터와 동일하다는 것을 보장할 수가 없는 것이죠.
  한 예로, 운영체제의 TCP/IP 스택이 3개의 패킷을 받았다고 가정해봅시다.
  
 <원문 사이트의 puml 파일 누락>
 
  흐름 기반 프로토콜의 일반적인 특성으로 인해, 높은 확률로 수신측에서는 다음과 같이 읽어들였을 겁니다.
 
 <원문 사이트의 puml 파일 누락>
 
  따라서, 서버나 클라이언트를 떠나 어찌됐든 수신측에서는 수신한 데이터를 응용단에서 활용할 수 있는 유의미한 단위로 분할해주는 일련의 작업이 필요하겠죠. 상기 예제의 경우라면 아래와 같이 분할해줄 수 있을 겁니다:

 <원문 사이트의 puml 파일 누락>
  

첫번째 해결

 TIME 클라이언트 예제로 잠깐 다시 돌아가보도록 합시다. 여기서도 동일한 문제가 있으니까요. 32-bit 정수는 굉장히 작은 크기의 데이터이기 때문에 잘못 분할되는 일이 잦지는 않을 겁니다. 하지만, 문제는 그런 일이 발생할 수 있다는 것이고, 트래픽이 늘어나면 그 발생 확률이 높아질 것이라는 점입니다.
 
  가장 간단한 해결방법은 내부 버퍼가 수신된 데이터를 누적하도록 만드는 것이 되겠습니다. 수신한 데이터가 4-byte가 될 때까지 기다리도록 만드는 것이죠. 이 해결법을 도입하기 위해 수정된 TimeClientHandler 코드가 아래와 같습니다.
  
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package io.netty.example.time;
    import java.util.Date;
    public class TimeClientHandler extends ChannelInboundHandlerAdapter {
        private ByteBuf buf;
        
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) {
            buf = ctx.alloc().buffer(4); // (1)
        }
        
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) {
            buf.release(); // (1)
            buf = null;
        }
        
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf m = (ByteBuf) msg;
            buf.writeBytes(m); // (2)
            m.release();
            
            if (buf.readableBytes() >= 4) { // (3)
                long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
                System.out.println(new Date(currentTimeMillis));
                ctx.close();
            }
        }
        
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            cause.printStackTrace();
            ctx.close();
        }
    }
    cs

  • 1.ChannelHandler는2 사이클의 handlerAdded()와 handlerRemoved()의 리스너 메서드를 가집니다. 개발자는 지나치게 긴 시간동안 다른 프로세스의 개입이 차단되는 작업이 아니라면, 원하는 작업을 추가하여 해당 메서드들을 일종의 생성자와 소멸자로 이용할 수도 있습니다.

  • 2. 우선, 모든 수신된 데이터는 buf 버퍼안에 누적되게 됩니다.

  • 3. 그리고 buf 버퍼에 충분한 데이터(이 예제에서는 4 bytes)가 누적되었는지를 검사한 뒤, 실질적인 비지니스 로직을 시작합니다. 수신 데이터가 4 bytes가 되지 않을 경우, Netty는 channelRead() 메서드를 다시 호출하여, 추가적으로 데이터를 수신할 것이고, 궁극적으로는 총 4 bytes의 수신 자료를 획득하게 될 겁니다.

두번째 해결방법

 첫번째 해결방법으로 TIME 클라이언트에 발생했던 문제를 해결할 수는 있었지만, 수정된 핸들러가 그리 깔끔해보이지는 않는다는 것을 알 수 있습니다. 길이가 가변적인 필드를 포함하는, 복수개의 필드로 구성된 보다 복잡한 프로토콜을 첫번째 해결방법으로 제작한다고 한다면, 개발된 ChannelInboundHandler 구현체는 순식간에 유지보수가 불가능할 정도로 복잡해지고, 이해하기도 어려워질 겁니다.
 
 앞선 예제들에서 아마 독자는 ChannelPipeline에 2개 이상의 ChannelHandler를 추가할 수 있다는 것을 겪어봤을 겁니다. 두번째 해결방법은 이를 이용하는 것인데, 하나의 큼지막했던 ChannelHandler를 모듈단위로 쪼개듯이 여러 개의 핸들러로 나누어 처리하는 겁니다. 이를 통해 개발된 프로그램의 복잡도를 줄일 수 있죠.
  예로써, TimeClientHandler를 2개의 핸들러로 분할하는 것을 들 수 있습니다.
  
  • 수신 데이터가 유의미한 단위로 분할될 수 있도록 하는 TimeDecoder와,
  • 초기의 TimeClientHandler로 나누는 것입니다.

 다행스럽게도 Netty에서는 위의 첫번째 항목을 쉽게 구현할 수 있도록, 상속 가능한 클래스를 제공하고 있습니다.
 
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package io.netty.example.time;
    public class TimeDecoder extends ByteToMessageDecoder { // (1)
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
            if (in.readableBytes() < 4) {
                return// (3)
            }
            
            out.add(in.readBytes(4)); // (4)
        }
    }
    cs

  • 1.ByteToMessageDecoder는 ChannelInboundHandler의 구현체로, 수신데이터 분할 이슈를 해결하게 됩니다.

  • 2. ByteToMessageDecoder는  내부 누적버퍼를 가지고 있으며, 새로운 데이터가 수신될때마다 decode() 메서드를 호출합니다.

  • 3. decode()는 누적 버퍼에 충분한 데이터가 쌓이지 않은 경우, out에 아무런 동작도 수행하지 않습니다. 충분한 데이터가 쌓였을 경우, 즉, 이 예제에서는 4 bytes의 데이터가 버퍼에 누적되면 ByteToMessageDecoder가 decode() 메서드를 호출할 것입니다.

  • 4. decode() 메서드가 out에 객체를 추가했다는 것은, decoder가 메시지를 성공적으로 유의미하게 분할했다는 것을 의미합니다. 이후 즉각적으로 ByteToMessageDecode는 누적 버퍼에 남아있는 데이터는 폐기합니다. 여러 개의 메시지를 decode하기 위해 별도의 코드가 필요하지 않다는 것을 기억하시기 바랍니다. ByteToMessageDecoder가 더 이상 out에 추가할 객체가 없을 때까지 decode() 메서드를 호출하기 때문입니다.

 이제 ChannelPipeline에 삽입할 핸들러가 한 가지 늘어났으니, TimeClient 코드에서 ChannelInitializer 구현체 소스를 수정하겠습니다.
 
  • 1
    2
    3
    4
    5
    6
    b.handler(new ChannelInitializer<SocketChannel>() {
        @Override
        public void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
        }
    });
    cs

  만약 모험적인 성향이 강한 개발자라면, decoder를 더욱 간소화할 수 있는 ReplayingDecoder를 사용해보고 싶을지도 모르겠습니다. 추가적인 정보를 위해서는 API 레퍼런스를 참조하기 바랍니다.
  
  • 1
    2
    3
    4
    5
    6
    7
    public class TimeDecoder extends ReplayingDecoder<Void> {
        @Override
        protected void decode(
                ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
            out.add(in.readBytes(4));
        }
    }
    cs

 추가적으로 Netty는 사용자들이 유지보수 불가능한 덩어리 핸들러를 만들어야만 하는 상황에 처해지지 않도록, 대부분의 프로토콜을 손쉽게 이용할 수 있는 out-of-the-box 디코더를 제공합니다. 상세한 예제 코드는 다음 패키지들에 포함되어 있습니다.
 
  • io.netty.example.factorial - binary 프로토콜에 유용하게 사용할 수 있습니다.
  • io.netty.example.telnet - 텍스트 라인 기반 프로토콜에 유용하게 사용할 수 있습니다.

ByteBuf 대신 POJO(Plain Old Java Object)를 사용하기

 지금까지 살펴본 모든 예제에서는 프로토콜 메시지의 기본 데이터 구조로써 ByteBuf를 사용해왔습니다. 이번에는 TIME 프로토콜 서버 및 클라이언트 예제를 ByteBuf가 아닌, POJO를 사용하여 구현해보도록 하겠습니다.
 
 ChannelHandler에서 POJO를 사용함으로써 얻어지는 이점은 명확합니다. 핸들러에 대한 유지보수와 ByteBuf에서 필요한 데이터를 추출해내는 코드를 핸들러 바깥에서 재사용하기에도 용이해진다는 것이죠.  TIME 서버와 클라이언트 예제에서는 32-bit 정수만을 다루었기에 ByteBuf를 직접적으로 이용하는 것에 대해 별다른 무리가 없었습니다만, 실제로 사용되는 프로토콜을 구현하기는데는 상당한 무리가 따르기에, 이같은 이점을 활용할 필요가 있습니다.

 우선 UnixTime이라는 클래스를 하나 정의해보도록 하겠습니다.
 
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package io.netty.example.time;
    import java.util.Date;
    public class UnixTime {
     
        private final long value;
        
        public UnixTime() {
            this(System.currentTimeMillis() / 1000L + 2208988800L);
        }
        
        public UnixTime(long value) {
            this.value = value;
        }
            
        public long value() {
            return value;
        }
            
        @Override
        public String toString() {
            return new Date((value() - 2208988800L) * 1000L).toString();
        }
    }
    cs

 이제 TimeDecoder로 하여금 ByteBuf 대신 UnixTime을 만들도록 바꿔야겠죠.


  • 1
    2
    3
    4
    5
    6
    7
    @Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            return;
        }
     
        out.add(new UnixTime(in.readUnsignedInt()));
    }
    cs

 수정된 디코더를 보면 TimeClientHandler가 더이상 ByteBuf를 사용하지 않는 것을 알 수 있습니다.
 
  • 1
    2
    3
    4
    5
    6
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        UnixTime m = (UnixTime) msg;
        System.out.println(m);
        ctx.close();
    }
    cs
 
 훨씬 간단하고, 또 보기 좋지 않나요? 동일한 기법이 서버측에도 활용될 수 있습니다. TimeServerHandler도 수정해봅시다.
 
  • 1
    2
    3
    4
    5
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ChannelFuture f = ctx.writeAndFlush(new UnixTime());
        f.addListener(ChannelFutureListener.CLOSE);
    }
    cs

 이제 UnixTime을 다시 ByteBuf로 바꿔주는 ChannelOutboundHandler 구현체인 인코더(encoder)만 남았습니다. decoder보다 훨씬 간단하게 작성되는데, 이는 메시지를 부호화(encoding)할 때에는 패킷 분할을 고려할 필요가 없기 때문이죠.
 
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package io.netty.example.time;
    public class TimeEncoder extends ChannelOutboundHandlerAdapter {
        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
            UnixTime m = (UnixTime) msg;
            ByteBuf encoded = ctx.alloc().buffer(4);
            encoded.writeInt((int)m.value());
            ctx.write(encoded, promise); // (1)
        } 
    cs

  • 1. 이 라인에는 짚고 넘어가야할 부분이 2가지가 있습니다.
  • 첫째로, 부호화된 데이터에 대한 버퍼 기록 및 실제 송신까지에 대한 처리 결과를 성공 혹은 실패의 형태로 Netty가 기록할 수 있도록, ChannelPromise 객체를 원형 그대로 넘겨주는 것입니다.
  • 둘째로는 ctx.flush() 메서드를 호출하지 않은 것입니다. 왜냐하면 flush() 메서드를 override 할 목적으로 작성된 별도의 핸들러 ㅔ서드인 void flush(ChannelHandlerContext ctx)가 존재하기 때문입니다.

 추가적인 간소화를 위해서, MessageToByteEncode 객체를 아래와 같이 활용할 수도 있습니다:
 
  • 1
    2
    3
    4
    5
    6
    public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
        @Override
        protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
            out.writeInt((int)msg.value());
        }
    }
    cs

 마지막으로 남은 작업은 TimeServerHandler에 앞서, 서버측 ChannelPipeline에 만들어진 TimeEncoder를 넣어주는 것이 되겠습니다. 이 부분은 직접 수행해보면서 결과를 확인하면 되겠습니다.
 

 개발된 어플리케이션을 종료하기

 Netty 어플리케이션을 종료한다는 것은, 개발자가 생성한 모든 EventLoopGroup들을 종료시킨다는 것으로, 보통은 shutdownGracefully() 메서드를 통해 간단히 수행할 수 있습니다. 이 메서드는 Future를 반환하는데, 이 Future를 통해, EventLoopGroup와 해당 group에 종속된 모든 Channel들이 완전하게 종료되었다는 것을 알 수 있습니다.
 

 마치며

 본 문서에서 우리는 Netty를 이용하여 온전하게 동작하는 네트워크 어플리케이션을 어떻게 만들 수 있는지에 대해 빠르고 간편하게 살펴보았습니다. 다음에 제공될 예정인 문서에서는 Netty에 대한 추가적인 정보들이 포함될 것입니다. 독자분들이 io.netty.example 패키지에 포함되어 있는 Netty 예제들을 살펴 본다면, Netty를 이해하고 사용하는데 있어 보다 큰 편익을 얻을 것이라 생각됩니다.
  Netty Community는 독자들의 질문과 제안에 항상 열려있다는 것을 기억해시길 바라며, 본 문서를 마칩니다.





도움이 되셨다면 따뜻한 (꼭 따뜻한.. 중얼중얼..) 댓글을 부탁드립니다.


실컷 다 번역해놓고 다른 분께서 이미 번역해놓으신 게 있다는 걸 발견했습니다.


이래서 검색이 중요합니다 여러분.. 머리가 나쁘면 손발이 고생을 한다더니ㅠㅠ


하지만 번역하면서 기본적인 내용에 대해 알게되고, 이해한 부분이 적지 않으니까!


나름의 소용은 있었다고 스스로를 위안하면서 글을 줄입니다.


15년에 작성된 최익필님의 네티 가이드 링크도 같이 남겨놓습니다.

자작프로그램/AHK-PC

프리젠터 비정상 동작 방지용 프로그램 (기본)


가장 하단으로 스크롤 하시면 스크립트랑 프로그램 있습니다.






저는 라스맥사의 LR-60G 라는 모델명의 그린 레이저포인터 겸 프리젠터를 사용하고 있습니다.


다 좋은데 이거 PC에 따라서 버튼을 한 번만 눌렀는데 2-3페이지가 한번에 넘어가는 현상이 생기는 경우가 있습니다.


이게 딱 특정 PC도 아니고.. 업무상 PC를 다룰 일이 굉장히 많은데, 2자릿수 이상의 PC에 연결해서 테스트 해봤는데


1/3 이상에서 이런 현상이 나타나더군요.



정말 환장하게 찝찝합니다......


뽑기가 잘못됐나 싶어서 새 제품으로 교환도 받아봤는데 똑같더군요.


후... 그냥 다음부터는 비싸고 좋은 거 사 써야지 하고는...!



하고는 잘 잊고 지냈는데...


소파에 누워서 컴퓨터로 예능을 보다가 문득(!) 생각이 난 겁니다.


, 이걸 리모컨으로 쓰면 덜 귀찮겠구나!!!!!!!!!!




그래서 리모컨으로 쓰려고 스크립트를 짤려니.. 욕심이 나더라는거죠.


앞으로 가기, 뒤로 가기, 볼륨 업/다운과 재생/정지를 넣고 싶더라는 겁니다.


클릭 / 더블 클릭 / 롱클릭에 필요하다면 트리플클릭을 넣자며 룰루랄라 신나게 구상 다 해놨더니


여기서 또 걸리는게 저놈의 오동작!!!!!!


으아아아아머닝러ㅏ비쟈어ㅣ랴벋리ㅑㅠㅓ비ㅑㅓㄱㅈ다ㅣ햐ㅏ버ㅣ야러히뱌더기ㅑ허비댜ㅓㄱㅎ




그래서 한번 더 마음이 상했습니다.


왜 이런 물건을 사게 된 걸까요... 


주도면밀한 자아성찰과 무한히 부정적인 자기반성을 겪으면 답을 알 수 있게 될려나요...




아무튼,


일단 프리젠터로라도 쓸 수는 있게 만들어야겠다 싶어서 하나 짜서 올려놓습니다.


제가 나중에 쓸 일 있을 때 빨리 찾아쓸 욕심으로.. 후후후...


열악한 오동작을 딛고, 깔끔하게 동작하는 리모컨을 어떻게 만들어낼지는 조금 더 고민을 해봐야 겠습니다.


위가 소스, 아래가 실행 파일입니다.


just-once.ahk

just-once.exe



현재 오동작 내용은 버튼 클릭 1번에 D 메시지가 2번~3번, U 메시지가 1번 발생합니다.


현재 스크립트를 쓰게 되면 D와 U 메시지가 짝으로 발생하지 않으면 키 입력을 무시합니다.


혹시 같은 현상에 고통받는 분이 있다면 도움이 되길...

  1. 수정/삭제 답글

    비밀댓글입니다

    • Favicon of http://rahs.tistory.com the Earnest Rahs 수정/삭제

      오토핫키(AHK) 라는 스크립트언어로 만들어진 스크립트들입니다~

자작프로그램/AHK-PC

AHK(Autohotkey)를 업무에 활용하기 (프랑슘 키보드로 디버그 하기)















부제는 덱 프랑슘 키보드(DECK CBL87N) F11 F12 좀 쉽게 누르기 정도가 되겠습니다.



Visual Studio 환경에서 개발을 하다보니 필연적으로 F11키와 F12키에 대한 접근성이 요구가 되는데,


덱 프랑슘 키보드를 사용해보신 분 아시겠지만 매크로 토글키가 F11 위치에, 그리고 Pn 키가 F12 위치에 있어서,


F11키를 누르려면 Fn키 + 매크로 토글 키를.. F12키를 누르려면 심지어 Fn키 + ESC 키를 눌러야만 합니다.


아래 사진 보시면 감이 오실텐데.. 디버그 할 때 얼마나 불편할지 짐작이 되실 겁니다.





그래서,


AHK를 이용해서 간단하게 F9 ~ F12를 각 Alt+1부터 Alt+4까지 키 리맵핑을 했습니다.


#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

#SingleInstance

SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.


!1::

Send, {F9}

return


^+!1:: ; Ctrl + Shift + F9 모든 브레이크 포인트 해제

Send, ^+{F9}

return


; Ctrl + F9 현위치 설정된 브레이크 포인트 해제

^!1::

Send, ^{F9}

return


!2::

Send, {F10}

return


^!2:: ; Ctrl + F10 커서가 있는 곳까지 실행

Send, ^{F10}

return


!3::

Send, {F11}

return


+!3:: ; Shift + F11 현 함수 빠져나감

Send, +{F11}

return


!4::

Send, {F12}

return


이래놨더니, PC를 켤 때마다 이걸 실행시켜야 하는 겁니다.


해서, 이걸 시작 프로그램에 넣으려고 생각해봤더니..


그럼 나중에 다른 자동화 프로그램을 추가하게 되면..? 이라는 생각이 드는 겁니다.


파일 하나에 계속 기능을 추가해도 되겠지만, 필요에 따라 좀 켜고 끄고 할 수가 있었으면 좋겠다는 생각이 든 거죠.



그래서,


아래와 같은 경로를 생성한 뒤에 위 스크립트의 소스와 실행파일을 targets 폴더 안에 넣어주었습니다.


.../메인 스크립트 위치/targets


그리고 메인 스크립트 위치에 아래 스크립트의 소스와 실행파일을 넣어주었습니다.


#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.

#SingleInstance

SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.

SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.


Loop Files, %A_ScriptDir%\targets\*.exe

{

Run, %A_LoopFileFullPath%

}

ExitApp



그리고 이 메인 스크립트(가칭)의 실행파일의 바로가기를 만들어서 윈도우 시작프로그램에 등록해주었습니다.



이제 부팅될 때마다 이 메인 스크립트가 동작을 합니다.


스스로가 위치한 폴더 바로 아래의 targets 폴더를 뒤져서 안에 있는 exe파일을 모조리 1번씩 실행시키고,


스스로는 종료시키는 스크립트입니다.



이제 별도 스크립트가 필요할 경우,


저는 저 targets 폴더 안에 실행파일을 추가해주기만 하면 되는 겁니다*ㅁ*



뭐, 추가했는데 안 쓰게 된 target 스크립트가 생긴다면, 그냥 경로를 옮겨주거나, exe파일만 지워주면 되는 거고,


다시 필요해지면 컴파일만 하면 다음 부팅 때부터 다시 실행이 되는 거니까 관리하기가 한결 쉬워지는 이점이 있거든요!!


라고 생각합니다.



혹시 더 좋은 방법 있으신 분 계시면 댓글로 깨우쳐 주시면 감사드리겠습니다 :)


이만 총총..

알림

이 블로그는 구글에서 제공한 크롬에 최적화 되어있고, 네이버에서 제공한 나눔글꼴이 적용되어 있습니다.

카운터

Today : 21
Yesterday : 156
Total : 423,906