RPC 프레임워크 맨땅에서 헤딩하기 1탄

emptynote의 이미지

RPC 프레임워크를 자체 개발을 하기 위해서는 무엇을 해야 할까요?

(1) 데이터(=메시지)를 송수신 하는 네트워크 송수신 모듈이 필요하지요.
그리고 (2) 메시지 인코더와 디코더가 필요합니다.

처음 제가 RPC 프레임워크를 개발할 당시 Netty + Protocol-Buffers 라는 후보자가 있었습니다.
네트워크 송수신은 Netty 가 담당하고
메시지 인코더 디코더는 Protocol-Buffers 가 담당하니 너무나 좋은 조합이였습니다.

그렇지만 저는 핵심은 직접해야 한다는 조엘의 조언을 따르기고 했습니다.

물론 제가 생각하는 개발 프레임워크는 메시지 주도 개발이기에 IO 문서 OK 떨어지면

개발이 일사 천리로 진행할 수 있는 모습을 꿈꾸었었기에

아쉽게도 Protocol-Buffers 는 독자적인 메시지 정의 파일 포맷을 사용하기에 이 모습을 그릴 수 없어 포기한 면도 있습니다.

자 맨땅에 헤딩할려면 우선은 메시지 인코더 디코더를 먼저 만들어야 합니다.

아 그런데 저는 '그저 그런 개발자'기이에 '그저 그런 개발자' 답게 구현이 쉬운 스트림으로 IO 기초를 만들었습니다.

고정 크기를 갖는 스트림과 가변 크기를 갖는 스트림이 첫번째 발걸음입니다.

고정 크기는 구현및 검증이 쉽습니다.

가장 먼저 고정 크기 스트림을 튼튼하게 만들어 봅시다.

이것은 나중에 가변 크기에 대한 검증 도구로 재 사용됩니다.

그 다음은 가변 크기 입/출력 스트림을 구현해야 하는데 순차적인 버퍼 목록으로 구현을 하였습니다.

왜? 이렇게 하는것이 구현이 쉽기때문입니다.

그리고 가변 크기 입/출력 스트림을 할려면 우선은 버퍼 폴이 필요합니다.

왜냐하면 버퍼 할당과 해제 비용이 많이들기 때문에 버퍼 폴로 만들어서 사용하기 위함입니다.

이것에는 제 나름 노하우가 숨어 있습니다.

전 직장에서 다행히 socket pool 개선을 한적이 있었습니다.

문제 사례를 접하는데 진짜 상상을 뛰어 넘는 경우가 있다는것을 보게 되었습니다.

물론 폴에서 가장 필요한 기능인 폴 유실 책임자 색출하기는 기술상 해결을 못했지만 나름 제 경험치가 들어간 폴을 작성하였습니다.

정리하면

첫번재 고정 크기 스트림과
두번째 가변 크기 스트림용 버퍼 폴로
세번째 가변 크기 스트림

이렇게 차례대로 개발을 하시면 메시지를 송수신에 대한 기초 공사가 끝나게 됩니다.

이제는 2가지가 남았네요.

그러면 이제 Protocol-Buffers 처럼 메시지 정의 파일과 그 파일로 부터 메시지 관련 파일을 생성하도록 하는 도구를 만들 차례입니다.

메시지 정의 파일은 xml 파일로 정했고

'메시지 정의 파일' 인 xml 의 구조를 정의한 xss 파일에 대해서는 아래 내용을 참조해 주시기 바랍니다.

'메시지 정의 파일'을 바탕으로 IO 관련 파일들을 만들어 주면 됩니다.

'메시지 정의 파일' 로 부터 생성되는 파일은 총 5개로

<메시지식별자>.java ===> 메시지 내용을 담기는 VO 입니다. <메시지식별자>ClientCodec.java ===> 방향성을 구현한 클라이언트 코덱입니다. <메시지식별자>ServerCodec.java ===> 방향성을 구현한 서버 코덱입니다. <메시지식별자>Encoder.java ===> 메시지 내용을 중간 단계 객체로 만들어 주는 인코더입니다. <메시지식별자>Decoder.java ===> 스트림을 중간 단계 객체로 만들어 주는 디코더입니다.

입니다.

메시지 정의 파일로 부터 모델을 구축하여 모델을 바탕으로 생성하기입니다.

좀더 관심이 있으시면 "kr.pe.codda.common.message.builder.info" 패키지에 있는 파일들을 참고하시기 바랍니다.

저두 나름 자바 즉 객체지향 언어를 다루는 개발자이기때문에

사용자가 정의한 프로토콜을 추가할 수 있는 구조로 설계를 하였습니다.

과거 버전에는 json, xml 프로토콜을 넣었다가 개편을 하면서 지금은 삭제된 상태입니다.

시간 여유가 되면 추가할 생각입니다.

기본 프로토콜로 DHB, THB 가 있으며

이진 프로토콜로 헤더 + 바디이며

헤더는 진짜로 별거 없습니다. 단순 '바디 크기'만 갖는것인데 DHB 는 헤더및 바디 MD5 가 추가되었을뿐입니다.

socket write 동작이 메시지 단위로 동기화가 되지 않으면 즉 쓰레드 세이프 하지 않으면 남의 데이터가 섞일 수 있는데 MD5 는 이를 감지하는데 좋은 도구이기때문에 MD5 를 갖는 DHB 를 추가하였습니다.

아쉽게도 저는 제가 만든 프로그램이 쓰레드 세이프하다는것을 수학적으로 검증하지 못했습니다.

부족하지만 DHB 와 랜덤값을 갖는 에코 메시지로 쓰레드 세이프함 검증을 대신하고 있습니다.

이것으로 충분한지는 잘 모르겠습니다.

이진 스트림을 바탕으로 하는 데이터 송수신시 가장 성가신 것이 1 byte 어긋남으로 대표되는 문제인데

이 문제를 어떻게 해결을 할까? 고민을 하던중에

Protocol-Buffers 포함하여 직렬화 라이브러리 여러가지를 비교를 하는 곳에서 보니

데이터 타입이 있어 냉큼 차용을 했습니다.

왜냐? 데이터 타입은 이진 스트림에서 xml 이나 json 처럼 IO 구조를 스스로 설명하게 해주게 하는 마법을 가능하게 해주기때문입니다.

혼자 개발하는데도 아주 가끔은 한쪽만 IO 변경을 하는 경우가 있는데

그때 어디가 문제인지를 보여주기에 좋네요.

조엘이 말한 개밥 먹기의 중요성을 깨닫게 합니다.

이 업데이트할때 json, xml 프로토콜까지 신경 쓰기 싫어서 걍 삭제를 해네요.

>> BoardListReqDecoder.java 일부

singleItemEncoder.putValueToWritableMiddleObject(pathStack.peek(), "requestedUserID"
			, kr.pe.codda.common.type.SingleItemType.UB_PASCAL_STRING // itemType
			, boardListReq.getRequestedUserID() // itemValue
			, -1 // itemSize
			, null // nativeItemCharset
			, middleWritableObject);

>> BoardListReqEncoder.java 일부

boardListReq.setRequestedUserID((String)
		singleItemDecoder.getValueFromReadableMiddleObject(pathStack.peek()
			, "requestedUserID" // itemName
			, kr.pe.codda.common.type.SingleItemType.UB_PASCAL_STRING // itemType
			, -1 // itemSize
			, null // nativeItemCharset
			, middleReadableObject));

프로토콜 핵심 메소드는

(1) public ArrayDeque M2S(AbstractMessage inputMessage, AbstractMessageEncoder messageEncoder)
(2) public void S2MList(ReceivedDataStream receivedDataOnlyStream, ReceivedMessageBlockingQueueIF wrapMessageBlockingQueue)

입니다.

프로토콜은 각 데이터 타입별로 프로토콜에 맞도록 인코더/디코더를 구현을 하면 됩니다.

프로토콜 클래스 설계의 핵심 키워는 새로운 사용자 정의 프로토콜 추가할 수 있어야 한다는것이고

프로토콜을 바꾸어도 메시지 내용은 그대로여야 한다는것입니다.

Protocol-Buffers 는 강력한 직렬화 라이브러리입니다.

그것에 비하면 많이 모자라지만 웹에서만 만큼은 적어도 자체 커뮤니티에 필요한 응용 정도에서는

딱히 부족함을 못느끼겠습니다. 독자 직렬화 라이브러리를 고집할지 다른 대안으로 갈아 탈지는 아직도 고민중이지만 메시지 정보 파일로 부터 IO 파일 생성 도구가 빈약한것일뿐

실질적으로 어떠한 VO 라도 직렬화/역직렬화를 만들 수 있습니다.

그렇지만 이렇게 수동으로 만든 IO 는 관리 하기가 참 어렵습니다.

그리고 IO 갯수가 늘어날 수록 안쓰는 IO 찾기가 점점 더 어려워지더라구요.

메시지 정보 파일만 잘 관리하여 전체 삭제후 생성기 통해서 생성하면 쉬운데

매번 수동으로 작성한 IO 는 따로 수작업 처리해 주어야 하니 매우 번거롭네요.

이것에 대한 대안이 필요하다고 생각합니다.

Netty 와 무엇이 차이나냐? 물으신다면 바로 이점입니다.

저는 응용어플 개발자라 응용어플 개발자한테 필요한것을 제공해 주는 프레임워크를 지향합니다.

이 프레임워크는 RPC 프레임워크에 익숙한 제가 이용하기 만들고 있는것입니다.

이제 프로토콜을 저작도구를 갖추게 되었습니다.

이제 남은것은 네트워크 송수신입니다.

다음 편에 네트워크 송수신에 대해서 떠들어 보겠습니다.

>> 메시지 파일 구조 정의 xss 파일

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
	<xs:group name="itemgroup">
		<xs:choice>
			<xs:element name="singleitem">
				<xs:complexType>
					<xs:attribute name="desc" use="optional">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>	
 
					<xs:attribute name="name" use="required">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
 
					<xs:attribute name="type" use="required">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:enumeration value="byte" />
								<xs:enumeration value="unsigned byte" />
								<xs:enumeration value="short" />
								<xs:enumeration value="unsigned short" />
								<xs:enumeration value="integer" />
								<xs:enumeration value="unsigned integer" />
								<xs:enumeration value="long" />
								<xs:enumeration value="ub pascal string" />
								<xs:enumeration value="us pascal string" />
								<xs:enumeration value="si pascal string" />
								<xs:enumeration value="fixed length string" />
								<xs:enumeration value="ub variable length byte[]" />
								<xs:enumeration value="us variable length byte[]" />
								<xs:enumeration value="si variable length byte[]" />
								<xs:enumeration value="fixed length byte[]" />
								<xs:enumeration value="java sql date" />
								<xs:enumeration value="java sql timestamp" />
								<xs:enumeration value="boolean" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
 
					<xs:attribute name="size" use="optional">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
 
					<xs:attribute name="charset" use="optional">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
 
					<xs:attribute name="defaultValue" use="optional">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
				</xs:complexType>
			</xs:element>
 
			<xs:element name="array">
				<xs:complexType>
					<xs:sequence>
						<xs:group minOccurs="0" maxOccurs="unbounded" ref="itemgroup" />
					</xs:sequence>
 
					<xs:attribute name="name" use="required">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
					<xs:attribute name="cnttype" use="required">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:enumeration value="reference" />
								<xs:enumeration value="direct" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
					<xs:attribute name="cntvalue" use="required">
						<xs:simpleType>
							<xs:restriction base="xs:string">
								<xs:minLength value="1" />
							</xs:restriction>
						</xs:simpleType>
					</xs:attribute>
				</xs:complexType>
			</xs:element>
		</xs:choice>
	</xs:group>
 
	<xs:element name="codda_message">
		<xs:complexType>
			<xs:sequence>
				<xs:element name="messageID" minOccurs="1" maxOccurs="1">
					<xs:simpleType>
						<xs:restriction base="xs:string">
							<xs:pattern value="[a-zA-Z][a-zA-Z1-9]+" />
						</xs:restriction>
					</xs:simpleType>
				</xs:element>
				<xs:element name="direction" minOccurs="1" maxOccurs="1">
					<xs:simpleType>
						<xs:restriction base="xs:string">
							<xs:pattern value="[a-zA-Z][a-zA-Z1-9_]+" />
						</xs:restriction>
					</xs:simpleType>
				</xs:element>
				<xs:element name="desc" type="xs:string" minOccurs="0" maxOccurs="1" />
				<xs:group minOccurs="0" maxOccurs="unbounded" ref="itemgroup" />
			</xs:sequence>
		</xs:complexType>
	</xs:element>
</xs:schema>
Hodong Kim@Google의 이미지

내용에 오해의 소지가 있어 보강했습니다.

RPC 라는게 remote procedure call 입니다. 원격 프로시져 호출.
(DBus는 IPC 입니다. IPC 는 프로세스간 통신, 로컬에서 작동)
리눅스 사용하시면서 DBus 라는 용어 흔히 들어보셨을텐데
ibus 입력기가 DBus 를 이용하여 구현한 입력기입니다. 그래서 이름이 ibus 에요.
fcitx 도 DBus 를 사용하고 있습니다.

nimf (구 dasom)도 마찬가지로 IPC/RPC 를 다룹니다. unix socket 을 사용합니다.
(unix socket 를 사용하여 로컬로 한정되므로 IPC, 그냥 socket 을 사용하면 nimf 가 입력 RPC 프레임워크가 됩니다. 비동기 함수 넣어야겠죠.)
https://nimf-i18n.gitlab.io/reference/NimfIM.html 여기 나오는 함수들은 클라이언트에서 호출하는 함수들입니다.

void 	nimf_im_focus_in ()
void 	nimf_im_focus_out ()
void 	nimf_im_reset ()
gboolean nimf_im_filter_event ()
void 	nimf_im_set_cursor_location ()
void 	nimf_im_set_use_preedit ()

위 함수들을 호출하면 서버 측에 https://nimf-i18n.gitlab.io/reference/NimfServiceIC.html 의 함수들이 실행됩니다. 말 그대로 (원격) 프로시져(함수) 호출입니다.

void 	nimf_service_ic_focus_in ()
void 	nimf_service_ic_focus_out ()
void    nimf_service_ic_reset () 
gboolean nimf_service_ic_filter_event ()
void 	nimf_service_ic_set_cursor_location ()
void 	nimf_service_ic_set_use_preedit ()

이게 동기화 방식으로 먹통 현상없이 작동됩니다.
스레드를 사용하지 않았고, polling 방식으로 디스패칭합니다.
https://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html

프로토콜도 본인이 직접 설계를 했지요.

https://gitlab.com/nimf-i18n/nimf/blob/master/libnimf/nimf-message.c
https://gitlab.com/nimf-i18n/nimf/blob/master/libnimf/nimf-message-private.h

nimf 가 일종의 RPC 프레임워크라는 얘기입니다. 다만, input method 로 기능이 제한되었을 뿐이죠.
입력기 설계를 2015년 1월 경에 시작했고,
C/S 구조 디자인을 5월에 시작했고, nimf (구 dasom)의 핵심 IPC/RPC 는 2015년 10월에 완성되었습니다.
https://gitlab.com/nimf-i18n/nimf/commit/dd5fb47acddf634cb16742ee1e0f69e31b14ece4
그 후로 지금까지 발견된 IPC/RPC 버그가 없습니다.

님께서는 저를 쓰레기라 생각하시는데, nimf (구 dasom) 란 물건이 어떤 물건인지 감이 오시나요?
nimf 기능을 입력기 프레임워크로 제한하지 않고, (비동기 함수 만들어 넣고) 기능을 웹으로 확장하면 님께서 만들고자 하는 물건이 되는거에요.