---
title: "OpenResty의 핵심: cosocket - API7.ai"
description: "cosocket은 다양한 lua-resty-* 비동기 라이브러리의 기반이 됩니다. 이 글에서는 cosocket의 기본 개념과 일반적인 API를 소개합니다."
url: "https://api7.ai/ko/learning-center/openresty/the-core-of-openresty-cosocket"
---

# OpenResty의 핵심: cosocket

API7.ai

October 28, 2022

[OpenResty (NGINX + Lua)](https://api7.ai/learning-center/openresty)

오늘은 OpenResty의 핵심 기술인 cosocket에 대해 배워보겠습니다.

이전 글에서 여러 번 언급했듯이, cosocket은 다양한 `lua-resty-*` 비차단 라이브러리의 기반입니다. cosocket이 없다면 개발자들은 Lua를 사용하여 외부 웹 서비스에 빠르게 연결할 수 없습니다.

OpenResty의 초기 버전에서는 Redis나 memcached와 같은 서비스와 상호작용하기 위해 `redis2-nginx-module`, `redis-nginx-module`, `memc-nginx-module`과 같은 C 모듈을 사용해야 했습니다. 이러한 모듈들은 여전히 OpenResty 배포판에 포함되어 있습니다.

그러나 cosocket 기능이 추가되면서 C 모듈은 `lua-resty-redis`와 `lua-resty-memcached`로 대체되었습니다. 이제는 더 이상 C 모듈을 사용하여 외부 서비스에 연결하지 않습니다.

## cosocket이란 무엇인가?

그렇다면 cosocket이 정확히 무엇일까요? cosocket은 OpenResty에서 사용되는 고유 명사입니다. cosocket이라는 이름은 `coroutine + socket`의 조합으로 이루어져 있습니다.

cosocket은 Lua의 동시성 기능과 NGINX의 기본 이벤트 메커니즘을 결합하여 비차단 네트워크 I/O를 가능하게 합니다. 또한 cosocket은 TCP, UDP, Unix Domain Socket을 지원합니다.

OpenResty에서 cosocket 관련 함수를 호출하면 내부적으로 다음과 같은 다이어그램과 같이 동작합니다.

![cosocket 관련 함수 호출](https://static.api7.ai/2022/10/22/635361da226a5.jpg?imageMogr2/format/webp)

이전 글인 [OpenResty 원리와 기본 개념](https://api7.ai/learning-center/openresty/openresty-getting-started)에서도 이 다이어그램을 사용했습니다. 다이어그램에서 볼 수 있듯이, 사용자의 Lua 스크립트에 의해 트리거된 모든 네트워크 작업은 코루틴의 `yield`와 `resume`을 갖게 됩니다.

네트워크 I/O를 만나면 네트워크 이벤트를 NGINX 리스너 목록에 등록하고 제어권(yield)을 NGINX로 넘깁니다. NGINX 이벤트가 트리거 조건에 도달하면 코루틴을 깨워 계속 처리(resume)합니다.

위 과정은 OpenResty가 현재 우리가 보는 cosocket API를 구성하는 connect, send, receive 등의 작업을 캡슐화하는 청사진입니다. 여기서는 TCP를 처리하는 API를 예로 들겠습니다. UDP와 Unix Domain 소켓을 제어하는 인터페이스도 TCP와 동일합니다.

## cosocket API 및 명령어 소개

TCP 관련 cosocket API는 다음과 같은 범주로 나눌 수 있습니다.

- 객체 생성: `ngx.socket.tcp`.
- 타임아웃 설정: `tcpsock:settimeout` 및 `tcpsock:settimeouts`.
- 연결 수립: `tcpsock:connect`.
- 데이터 전송: `tcpsock:send`.
- 데이터 수신: `tcpsock:receive`, `tcpsock:receiveany`, `tcpsock:receiveuntil`.
- 연결 풀링: `tcpsock:setkeepalive`.
- 연결 종료: `tcpsock:close`.

또한 이러한 API를 사용할 수 있는 컨텍스트에 특히 주의해야 합니다.

```
rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_
```

또한 강조하고 싶은 점은 NGINX 커널의 다양한 제한으로 인해 사용할 수 없는 환경이 많다는 것입니다. 예를 들어, cosocket API는 `set_by_lua*`, `log_by_lua*`, `header_filter_by_lua*`, `body_filter_by_lua*`에서 사용할 수 없습니다. 현재는 `init_by_lua*`와 `init_worker_by_lua*`에서도 사용할 수 없지만, NGINX 커널이 이 두 단계를 제한하지 않기 때문에 나중에 지원이 추가될 수 있습니다.

이러한 API와 관련된 `lua_socket_`으로 시작하는 8개의 NGINX 명령어가 있습니다. 간단히 살펴보겠습니다.

- `lua_socket_connect_timeout`: 연결 타임아웃, 기본값 60초.
- `lua_socket_send_timeout`: 전송 타임아웃, 기본값 60초.
- `lua_socket_send_lowat`: 전송 임계값(low water), 기본값 0.
- `lua_socket_read_timeout`: 읽기 타임아웃, 기본값 60초.
- `lua_socket_buffer_size`: 데이터 읽기 버퍼 크기, 기본값 4k/8k.
- `lua_socket_pool_size`: 연결 풀 크기, 기본값 30.
- `lua_socket_keepalive_timeout`: 연결 풀 cosocket 객체의 유휴 시간, 기본값 60초.
- `lua_socket_log_errors`: cosocket 오류 발생 시 로깅 여부, 기본값 `on`.

여기서도 볼 수 있듯이, 일부 명령어는 API와 동일한 기능을 가지고 있습니다. 예를 들어 타임아웃 설정과 연결 풀 크기 설정 등이 있습니다. 그러나 두 가지가 충돌할 경우 API가 명령어보다 우선순위가 높으며, 명령어로 설정된 값을 덮어씁니다. 따라서 일반적으로 API를 사용하여 설정하는 것을 권장하며, 이는 더 유연합니다.

다음으로, 이러한 cosocket API를 사용하는 방법을 이해하기 위해 구체적인 예제를 살펴보겠습니다. 다음 코드의 기능은 간단합니다. 웹사이트에 TCP 요청을 보내고 반환된 내용을 출력합니다:

```
$ resty -e 'local sock = ngx.socket.tcp()
sock:settimeout(1000) -- 1초 타임아웃
local ok, err = sock:connect("api7.ai", 80)
if not ok then
    ngx.say("failed to connect: ", err)
    return
end
local req_data = "GET / HTTP/1.1\r\nHost: api7.ai\r\n\r\n"
local bytes, err = sock:send(req_data)
if err then
    ngx.say("failed to send: ", err)
    return
end
local data, err, partial = sock:receive()
if err then
    ngx.say("failed to receive: ", err)
    return
end
sock:close()
ngx.say("response is: ", data)'
```

이 코드를 자세히 분석해 보겠습니다.

- 먼저, `ngx.socket.tcp()`를 사용하여 `sock`이라는 이름의 TCP cosocket 객체를 생성합니다.
- 그런 다음, `settimeout()`을 사용하여 타임아웃을 1초로 설정합니다. 여기서 타임아웃은 연결과 수신을 구분하지 않고 통일된 설정입니다.
- 다음으로, `connect()` API를 사용하여 지정된 웹사이트의 80번 포트에 연결하고, 실패하면 종료합니다.
- 연결이 성공하면, `send()`를 사용하여 구성된 데이터를 전송하고, 실패하면 종료합니다.
- 데이터 전송이 성공하면, `receive()`를 사용하여 웹사이트로부터 데이터를 수신합니다. 여기서 `receive()`의 기본 매개변수는 `*l`이며, 이는 첫 번째 줄의 데이터만 반환합니다. 매개변수를 `*a`로 설정하면 연결이 닫힐 때까지 데이터를 수신합니다.
- 마지막으로, `close()`를 호출하여 소켓 연결을 명시적으로 닫습니다.

보시다시피, cosocket API를 사용하여 네트워크 통신을 하는 것은 몇 단계만으로 간단합니다. 이제 이 예제를 더 깊이 탐구하기 위해 몇 가지 조정을 해보겠습니다.

**1. 소켓 연결, 전송 및 읽기 세 가지 작업에 대해 각각 타임아웃 시간을 설정합니다.**

우리가 사용한 `settimeout()`은 타임아웃 시간을 단일 값으로 설정합니다. 각각의 타임아웃 시간을 별도로 설정하려면 `settimeouts()` 함수를 사용해야 합니다. 예를 들어 다음과 같습니다.

```
sock:settimeouts(1000, 2000, 3000)
```

`settimeouts`의 매개변수는 밀리초 단위입니다. 이 코드는 연결 타임아웃을 `1`초, 전송 타임아웃을 `2`초, 읽기 타임아웃을 `3`초로 설정합니다.

OpenResty와 lua-resty 라이브러리에서 시간 관련 API의 대부분의 매개변수는 밀리초 단위입니다. 하지만 호출할 때 특별히 주의해야 할 예외도 있습니다.

**2. 지정된 크기의 내용을 수신합니다.**

방금 말했듯이, `receive()` API는 한 줄의 데이터를 수신하거나 지속적으로 데이터를 수신할 수 있습니다. 그러나 10K 크기의 데이터만 수신하려면 어떻게 설정해야 할까요?

이때 `receiveany()`가 필요합니다. 이는 이러한 요구를 충족시키기 위해 설계되었으므로 다음 코드를 살펴보세요.

```
local data, err, partial = sock:receiveany(10240)
```

이 코드는 최대 10K의 데이터만 수신한다는 의미입니다.

물론, `receive()`에 대한 또 다른 일반적인 사용자 요구 사항은 지정된 문자열을 만날 때까지 데이터를 계속 가져오는 것입니다.

`receiveuntil()`은 이러한 종류의 문제를 해결하기 위해 설계되었습니다. `receive()`와 `receiveany()`처럼 문자열을 반환하는 대신, 이터레이터를 반환합니다. 이렇게 하면 루프에서 호출하여 일치하는 데이터를 세그먼트로 읽고, 읽기가 완료되면 nil을 반환할 수 있습니다. 다음은 예제입니다.

```
local reader = sock:receiveuntil("\r\n")
     while true do
         local data, err, partial = reader(4)
         if not data then
             if err then
                 ngx.say("failed to read the data stream: ", err)
                 break
             end
             ngx.say("read done")
             break
         end
         ngx.say("read chunk: [", data, "]")
     end
```

`receiveuntil`은 `\r\n` 이전의 데이터를 반환하고, 이터레이터를 통해 한 번에 4바이트씩 읽습니다.

**3. 소켓을 직접 닫는 대신 연결 풀에 넣습니다.**

연결 풀이 없으면 새로운 연결을 생성해야 하며, 요청이 올 때마다 cosocket 객체가 생성되고 자주 파괴되어 불필요한 성능 손실이 발생합니다.

이 문제를 피하기 위해, cosocket 사용을 마친 후 `setkeepalive()`를 호출하여 연결 풀에 넣을 수 있습니다. 예를 들어 다음과 같습니다.

```
local ok, err = sock:setkeepalive(2 * 1000, 100)
if not ok then
    ngx.say("failed to set reusable: ", err)
end
```

이 코드는 연결 유휴 시간을 `2`초로, 연결 풀 크기를 `100`으로 설정합니다. 이렇게 하면 `connect()` 함수가 호출될 때 cosocket 객체를 연결 풀에서 먼저 가져옵니다.

그러나 연결 풀을 사용할 때 주의해야 할 두 가지가 있습니다.

- 첫째, 오류가 있는 연결을 연결 풀에 넣을 수 없습니다. 그렇지 않으면 다음에 사용할 때 데이터 전송 및 수신이 실패할 수 있습니다. 이것이 각 API 호출이 성공했는지 확인해야 하는 이유 중 하나입니다.
- 둘째, 연결 수를 파악해야 합니다. 연결 풀은 `Worker` 수준이며, 각 Worker는 자신의 연결 풀을 가지고 있습니다. 만약 10개의 `Worker`가 있고 연결 풀 크기를 `30`으로 설정했다면, 백엔드 서비스에 대해 300개의 연결이 생성됩니다.

## 요약

요약하자면, 우리는 cosocket의 기본 개념, 관련 명령어 및 API를 배웠습니다. 실용적인 예제를 통해 TCP 관련 API를 사용하는 방법에 익숙해졌습니다. UDP와 Unix Domain Socket의 사용도 TCP와 유사합니다. 오늘 배운 내용을 이해하면 이러한 모든 질문을 쉽게 처리할 수 있습니다.

cosocket은 상대적으로 사용하기 쉽고, 이를 잘 사용하면 다양한 외부 서비스에 연결할 수 있습니다.

마지막으로, 두 가지 질문을 생각해 볼 수 있습니다.

첫 번째 질문, 오늘의 예제에서 `tcpsock:send`는 문자열을 전송합니다. 만약 문자열로 구성된 테이블을 전송해야 한다면 어떻게 해야 할까요?

두 번째 질문, 보시다시피 cosocket은 많은 단계에서 사용할 수 없습니다. 그러면 이를 우회할 수 있는 방법을 생각해 볼 수 있을까요?

자유롭게 댓글을 남기고 공유해 주세요. 이 글을 동료와 친구들과 공유하여 함께 소통하고 발전할 수 있기를 바랍니다.

Share article link

Share

Related Posts

[![OpenResty FAQ | OpenResty의 실제 활용 사례](https://static.api7.ai/uploads/2023/02/12/jbv9M0zi_Hello%20World%20in%20OpenResty%20%283%29.png)](https://api7.ai/learning-center/openresty/faq-5)

## [OpenResty FAQ | OpenResty의 실제 활용 사례](https://api7.ai/learning-center/openresty/faq-5)

[![파트 3: OpenResty를 사용하여 마이크로서비스 API 게이트웨이 구축하기](https://static.api7.ai/uploads/2023/02/07/3XJXY9Jy_45.png)](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

## [파트 3: OpenResty를 사용하여 마이크로서비스 API 게이트웨이 구축하기](https://api7.ai/learning-center/openresty/how-to-build-a-microservices-api-gateway-part-3)

Table of Contents

- [cosocket이란 무엇인가?](#cosocket%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80)
- [cosocket API 및 명령어 소개](#cosocket-api-%EB%B0%8F-%EB%AA%85%EB%A0%B9%EC%96%B4-%EC%86%8C%EA%B0%9C)
- [요약](#%EC%9A%94%EC%95%BD)
