socket 의 C API 는 비동기 일기쓰기를 채용했으며, C를 호출하여 포트를 리슨할수 있으며 아니면 TCP 연결을 시작할수 있다. 구체 정의는 lua-socket.c 에 있다. 이 API 는 socketdriver 라고 불리우며, 구체적 사용은 GateServer 소스코드를 참고하여 학습할 수 있다. Framework 의 저수준의 스레드는, 결과를 PTYPE_SOCKET 형식의 메시지를 통해 원래 호출된 API 의 서비스로 전송한다. 구체적사항은 skynet_socket_poll을 보자 (skynet_socket.c)
실제로직에서, 비동기 API 는 사용하기 매우어렵다, 그리하여 TCP 소켓에 읽고 쓸수있는 블록 모드의 LuaAPI 또한 제공한다. 이것들은 socketdriver 의 WRAPPING 이다.
소위 블록모드는, 사실상 lua의 coroutine 메커니즘을 사용한다. socket api 를 호출했을 때, 서비스는 아마도 중지된다(시간단이 다른 작업 처리에 주어진다), 결과를 기다린후 socket 메시지를 통해 반환되고, coroutine 은 계속 실행된다.
socket api
local socket = require "skynet.socket"
이렇게 서비스내에 API 를 가져올 수 있다.
-
socket.listen(host, port, backlog) 클라이언트연결 감시
-
socket.open(address, port) TCP 연결생성. 숫자ID 반환, 연결생성시 현재 coroutine은 블럭된다, 연결 과정이 완료될 때까지
-
socket.close(id) 연결하나를 닫는다, 이 API 는 실행 흐름을 블럭할 수 있다. 왜냐하면 만약 다른 coroutine이 이 ID의 연결에대해 read 블록 read 작업중이였다면, read 작업이 끝난후에, close 작업이 return 하게 된다.
-
socket.close_fd(id) 아주 예외적인 경우에 볼수 있는 상황에서, 연결을 brute 하게 직접 끊어야할 필요가 있다, 게다가 socket.close 의 실행흐름을 블럭하는것을 피해야할경우, 이것을 사용한다.
-
socket.shutdown(id) 연결을 강제로 닫는다. close 와다른점은, 이것은 존재할 수 있는 다른 coroutine의 읽기 작업을 기다리지 않는다. 일반적으로 이API 의 사용을 추천하지 않는다., 하지만 만약 __gc 메타함수에서 연결을 끊어야 할경우, shutdown은 close 보다 좋은 선택이 될수 있다. (왜냐하면 gc 중에는 coroutine전환불가).
-
socket.read(id, sz) socket에서 sz로 지정한 문자수만큼 읽어들인다. 만약 지정한수만큼 읽어들이면, 이 문자열을 반환한다. 만약 연결이 끊어져서 문자열이 부족하면, 읽어들인 문자열에 false 를 붙여서 반환한다. 만약 sz 가 nil이라면, 읽어들일 수 있는만큼의 문자열을 읽어들인다. 하지만 적어도 한개의 바이트는 읽어들인다 (데이터가 없으면 블록된다)
-
socket.readall(id) socket에서 모든 데이터를 읽는다, socket연결이 끊어지거나, 다른 coroutine에 의해 socket.close 가 호출될때까지
-
socket.readline(id, sep) socket에서 한줄의 데이터를 읽는다, sep 는 행분할문자를 지정한다. 기본값은 “\n” 이다. 읽어들인 데이터에는 행분할자는 포함되지 않는다.
-
socket.block(id) 소켓에 읽을 데이터가 있을떄 까지 기다린다.
socket api 는 서로다른 두개의 쓰기 동작이 있다. skynet에서 각 socket 에는 대응하는 두개의 쓰기큐가 존재한다. 우리는 그냥 사용하면 된다.
-
socket.write(id, str) 하나의 문자열을 정상적인 큐에 넣으면, skynet frameowkr 는 쓸수 있을때 발송한다. 동시에 skynet 은 낮은 우선순위의 쓰기 동작을 제공한다(필요하지 않으면 안쓰면 된다)
-
socket.lwrite(id, str) 문자열을 낮은우선순위 큐에 넣는다. 만약 정상쓰기큐가 아직 완료되지 않았다면, 낮은우선순위큐의 데이터는 영원히 트리거되지 않는다. 오로지 정상쓰기큐가 비었을때, 낮은우선순위의 큐를 처리한다. 하지만 매번 문자열쓰기작업을 원자성작업으로 볼수 있다. 절반만 발송하고, 다시 일반큐의 데이터를 발송하는 일은 발생하지 않는다.
서버에 있어, 통상 포트의 감시가 필요하며, 특정연결의 처리권을 넘겨주는것이 필요하다. 그럴때 사용하는 API:
- socket.listen(address, port) 포트를 감시한다, id를 반환한다. start 함수에 사용한다.
- socket.start(id , accept) accept 는 함수이다. 모든 감시된id 에 대응하는 socket에 연결이 접근할때, accept함수를 호출하게 된다. 이 함수는 연결의 id 그리고 ip주소를 얻게 된다. 그리고 뒷처리를 하면 된다.
모든 accept 함수가 새로운 socket id 를 얻었을때, 즉시 이 socket 의 데이터를 받는것이아니다. 그 이유는 어쩔때 이소캣의 작업권리를 다른서비스에게 넘겨서 처리하고 싶을때 가 있기 때문이다.
socket 의 id 는 전체 skynet 노드에서 공개이다. 다시말해, 이 id 숫자를 통해 다른서비스에 메시지를 보낼수 있고, 다른서비스또한 소켓에 작업할 수 있다. 어떠한 서비스라도 socket.start(id) 를 호출한 이후, 이socket 의 데이터를 받을수 있게 된다. skynet framework 는 이 start api 를 호출한 위치에 따라서 대응하는 socket의 데이터가 어디로 전송될지 결정한다.
socket id 에 데이터를 쓰는것역시 먼저 start 의 호출 이 필요하다, 하지만 데이터를 쓰는건 start 를 호출한 동일한 서비스로 제한되지 않는다. 다시말해, 어떤 서비스중에 start 를 호출한후, 또다른 서비스중에 그 소켓에 데이터를 쓸수 있다. skynet 은 한번의 write 호출의 원자성을 보장한다. 즉, 만약 여러개의 서비스가 있고, 동시에 한개의 socket Id 에 데이터를 쓴다면, 각각의 쓰기작업의 내용은 분리될수 없음을 의미한다.
-
socket.abandon(id) 현서비스내의 데이터구조에서 socket id 를 삭제한다, 하지만 socket 을 닫지 않는다. 이는 id 를 다른서비스에 넘겨주어 소켓의 통제권을 넘길떄 사용될 수 있다.
-
socket.warning(id, callback) id 대응의 socket 에 발송대기 대기 데이터가 1M 바이트가 넘은후, 시스템은 callback 함수를 호출하여 경고한다. function callback(id, size) 콜백함수는 두개의 파라메터를 받으며 id와 size 이다, size 의 단위는 K 이다. 만약 callback 을 설정하지 않으면, 매번 64k 가 추가될떄 마다 skynet.error 를 사용하여 한줄의 오류 메시지를 남긴다. 일단 경고가 한번 발생되면, 버퍼가 비워질때 다시한번 size가 0인 메시지가 생성되어 버퍼가 비었음을 표시한다.
UDP
skynet 은 udp 프로토콜을 위해 제한적 지원을 한다. tcp 프로토콜과는 다르게, udp 프로토콜은 블록읽고쓰기가 필요없다. 이는 udp가 안정적인 프로토콜이 아니기 때문이다. 다음번에 읽어질 데이터 패키지가 무엇인지 예상할수 없다.(프로토콜이 순서와 패킷드롭이 없음을 보장하지 않음). 그래서 skynet의 udp 프로토콜 wraping은 callback 방식을 채용한다.
socket.udp 이 api 를 사용하여 udp 핸들을 생성할수 있으며, callback 함수를 바인딩해준다. 이핸들이udp 메시지를 받았을때, callback 함수가 트리거된다.
socket.udp(function(str, from), address, port) : id
- 첫번째 파라메터는 callback 함수이다, 두개의 파라메터를 받는다. str 은 문자열즉 받은 메시지내용이고, from 은 메시지를 보내온곳을 나타내는 문자열이다. 이문자열은 응답을 보낼때 사용된다. (socket.sendto를 보자)
- 두번째 파라메터는 바인딩된 IP 주소이다. 만약 넘겨주지 않으면, 기본값은 ipv4 의 0.0.0.0이된다.
- 세번째 파라메터는 하나의 숫자이다, 바인딩된 포트를 의미한다. 만약에 쓰지않거나 0을 넘겨주면, udp handle을 생성한다 (발송에사용됨), 하지만 고정된 포트에 바인딩되지 않는다.
이함수는 하나의 handle id 를 반환한다.
socket.udp_connect(id, address, port, callback)
udp handle 을 주어 기본 발송목적지를 설정할수 있다. socket.udp 를 사용하여 listen 하지 않은 정적인 handle을 만들때, 목적지를 설정할때 유용하다. 왜냐하면, socket.sendto에 전달할 유효한 주소문자열을 받을 방법이 없기 때문이다. 여기 callback 은 선택 사항이다, 일반적으로 socket.udp 로 handle 을 생성할때 callback 함수를 설정한다. 하지만 어쩔때, handle이 현재 서비스가 생성한것이아니고 다른곳에서 생성된것일 수 있다. 이럴경우 socket.start 를 사용하여 handle 의 소유권을 재설정해야하며, 이함수를 사용하여 callback 함수를 설정한다.
기본목적지를 설정한후, 이후 socket.write 함수를 사용하여 데이터를 발송 할수 있다.
주: handle은 하나의 서비스에만 속할수 있다. 하나의 핸들이 하나의 서비스에 귀속되었을때, skynet framework는 대응하는 네트워크 메시지를 전달한다. handle 을 향하여 네트워크데이터를 발송하는 서비스는 이핸들을 가지고 있을 필요가 없다.
socket.sendto(id, from, data)
네트워크 주소로 데이터패키지를 발송한다. 두번째 파라메터 from 은 네트워크 주소이다, 이것은 문자열이다, 일반적으로 callback 함수에서 생성된다. 주소문자열을 스스로 생성할수 없다. 하지만 callback 함수에서 얻은 주소문자열을 보존하여 이후에 사용한다. 발송하는 내용은 문자열 data 이다.
이 문자열은 socket.udp_address(from): address port 를 사용하여 읽을수 있는 IP주소와 포트로 변환할수 있다. 기록에 사용된다.
lualib/socket.lua 의 구현 코드를 읽어보면, socket api 가 어떻게 동작하는지 훨씬더 명백해 질것이다. 아마 위에 열거된 API 에 포함되지 않은것을 발견할것이다. 그것들은 나중의 버전에 폐기될것이다.
如果你需要一个网关帮你接入大量连接并转发它们到不同的地方处理。service/gate.lua 可以直接使用,同时也是用于了解 skynet 的 socket 模块如何工作的不错的参考。它还有一个功能近似的,但是全部用 C 编写的版本 service-src/service_gate.c 。
만약 대량의 접속과 서로다른곳에 전달하여 처리하는 게이트웨이가 필요하다면. service/gate.lua 를 그대로 사용할수 있으며, 동시에 skynet의 socket 모듈이 어떻게 동작하는지 이해하는데 나쁘지 않은 참고자료가 될것이다. 또한 비슷한기능의 것이 있는데, 전부 C 를 사용하여 작성한버전 service-src/service_gate.c 이다.
如果仅仅是了解 socket api 的基本用法,以及搭建一个简单的服务器。请参考 test/testsocket.lua ;udp 部分见 test/testudp.lua 。 만약 socket api 의 기본 사용법을 이해하고 싶을 뿐이라면, 그리고 간단한 서버를 만들고 싶다면. test/testsocket.lua 를 참고하고; udp 부분은 test/testudp.lua 를 참고하자.
이름서비스
skynet 의 저수준레벨에는, IP 가아닌 이름을 사용할때, 시스템api 인 getaddrinfo 를 사용한다, 전체 socket 스레드(현재 서비스만 블럭하는것이아니며, 전체 skynet 노드의 네트워크 메시지처리를 블럭함)를 블럭할 가능성이있다. 비록 대부분상황에서, 외부로 능동적으로 연결을 생성할 필요가 없다. 하지만 만약 httpc 와 같은 모듈을 사용하여 이름으로 외부에 요청할때, 절대로 고려해야할 이 문제가 있다.
skynet 暂时不打算在底层实现非阻塞的域名查询。但提供了一个上层模块来辅助你解决 dns 查询时造成的线程阻塞问题。 skynet 은 잠시 저수준에서 비동기의 이름검색의 구현을 고려하고있지 않다. 하지만 상층모듈에서 dns 검색이 일으키는 스레드 블럭 문제의 해결을 도와준다.
local dns = require "skynet.dns"
이렇게 이모듈을 로딩한다, 이것은 두개의 메서드가 있다.
사용하기전에, dns 서버를 필수로 설정해야한다.
- dns.server(ip, port) : port 기본값은 53. 만약 ip 를 쓰지 않을경우 /etc/resolv.conf 에서 적당한 IP 를 찾는다.
- dns.resolve(name, ipv6) : name 에 해당하는 IP 검색, 만약 ipv6이 true 라면 ipv6 주소를 검색한다. 기본값은 false 이다. 만약 검색이 실패하면 예외를 던진다. 성공하면 반환값은 ip 그리고 모든 ip 가 담긴 테이블이다.
- dns.flush() : 기본상황에서, 모듈은 TTL 값에 의거하여 검색결과를 cache 한다. 검색이 타임아웃할경우에도 이전의 결과를 반환한다. dns.flush()를 사용하여 cache 를 삭제할 수 있다. 주의: cache 보존은 호출자의 서비스에 보존된다. skynet 전체 프로세스에대해 보존되는게 아니다. 그러니 독립적인 dns 검색 서비스를 생성하여 dns 검색을 통일하여 처리하자
test/testdns.lua 의 예제를 읽어보자. 동시, 이 모듈은 skynet에서 udp 사용예제가 될수 있다.