ATT/GATT 심화: EATT, 큐 쓰기, Database Hash
1권 7장은 속성과 서비스·특성, 읽기·쓰기·알림을 다뤘다. 이 장은 현장에서 부딪히는 세부로 내려간다. 요청 하나가 끝나야 다음을 보낼 수 있는 순차 규칙과 30초 타임아웃, 긴 값을 나눠 쓰는 큐 쓰기, 여러 요청을 동시에 흘리는 EATT, 그리고 펌웨어를 올린 뒤 휴대폰이 옛 핸들을 계속 쓰는 문제를 막는 Database Hash와 Robust Caching이다.
- ATT opcode의 비트 구조와 여섯 PDU 종류, 순차 프로토콜과 30초 트랜잭션 규칙을 안다.
- Read Blob과 Prepare/Execute Write로 긴 값을 읽고 쓰는 과정을 따라갈 수 있다.
- EATT가 무엇을 바꾸는지(병렬 트랜잭션, 알림 신뢰성, 새 PDU)를 설명할 수 있다.
- Database Hash를 직접 계산하고, Robust Caching에서 서버가 0x12를 보내는 때를 판단할 수 있다.
ATT PDU와 순차 프로토콜
ATT PDU의 첫 바이트 opcode는 아래 6비트가 방법(Method), 비트 6이 명령 플래그(1이면 응답이 없는 명령), 비트 7은 6.3에서 “이전에 쓰던 값”이 되었다(서명된 쓰기의 인증 서명 플래그. 데이터 서명 삭제와 함께 0xD2 Signed Write Command도 사라졌다). 그래서 ATT_WRITE_CMD는 0x52 = 0x12(쓰기 방법) | 0x40이다. 나머지 바이트는 매개변수이고 ATT_MTU − 1까지 들어간다. 여러 바이트 값은 리틀 엔디언이지만, 속성 값 자체의 바이트 순서는 상위 사양이 정한다.
| 종류 | 방향 | 흐름 제어 | 예 |
|---|---|---|---|
| 요청(REQ) → 응답(RSP) | 클라이언트 → 서버 → 클라이언트 | 베어러마다 한 번에 하나 | READ 0x0A/0x0B, WRITE 0x12/0x13 |
| 명령(CMD) | 클라이언트 → 서버 | 없음, 처리 못 하면 버림 | WRITE_CMD 0x52 |
| 표시(IND) → 확인(CFM) | 서버 → 클라이언트 → 서버 | 베어러마다 한 번에 하나 | 0x1D / 0x1E |
| 알림(NTF) | 서버 → 클라이언트 | 없음 | 0x1B, MULTIPLE 0x23 |
요청-응답과 표시-확인 한 쌍이 트랜잭션이다. 트랜잭션은 한 베어러에서 끝나야 하고, 30초 안에 끝나지 않으면 실패로 처리하며 그 베어러로는 더 아무것도 보내지 않는다. 다시 쓰려면 베어러를 새로 맺어야 하는데, 고정 채널 0x0004 베어러라면 사실상 연결을 끊고 다시 맺는 것이다. 응답이 늦는 서버 하나가 휴대폰 앱 전체를 멈추게 하는 흔한 원인이다.
일반 ATT 베어러는 창구가 하나인 민원실이다. 앞사람 민원(요청)이 끝나야 다음 민원을 받는다. 서류 넣기(명령)와 안내 방송(알림)은 창구를 거치지 않지만, 손이 모자라면 버려질 수 있다. EATT는 창구를 여러 개 여는 것이다. 한 창구에서 긴 서류를 처리하는 동안 옆 창구에서 다른 민원을 본다.
PDU 종류(Table 3.1)·opcode 형식(Table 3.2): [Vol 3] Part F §3.3·§3.3.1 · 순차 프로토콜: §3.3.2 · 트랜잭션과 30초: §3.3.3 · opcode 요약과 0xD2: §3.4.8
긴 값: Read Blob과 큐 쓰기
ATT_MTU − 1보다 긴 값은 한 번에 오가지 못한다. 읽을 때는 ATT_READ_REQ로 앞부분을 받고, 이어서 ATT_READ_BLOB_REQ(0x0C)에 오프셋을 주어 나머지를 차례로 받는다(1권 7장의 왕복 계산). 쓸 때는 큐 쓰기를 쓴다. ATT_PREPARE_WRITE_REQ(0x16)로 (핸들, 오프셋, 조각)을 서버의 대기열에 넣고, 서버는 받은 내용을 그대로 되돌려 준다. 마지막에 ATT_EXECUTE_WRITE_REQ(0x18)에 플래그 1을 주면 대기열 전체를 한꺼번에 반영하고, 0을 주면 모두 버린다.
이 되돌려 받기를 이용한 것이 신뢰성 있는 쓰기(Reliable Writes)다. 클라이언트는 서버가 돌려준 조각이 보낸 것과 같은지 확인하고, 하나라도 다르면 실행 대신 취소(플래그 0)를 보낸다. 서버의 대기열이 가득 차면 Prepare Queue Full(0x09)이 온다. 대기열은 여러 특성의 조각을 함께 담을 수 있어 “여러 값을 원자적으로 바꾸기”에도 쓴다.
ATT_MTU가 23일 때 ATT_PREPARE_WRITE_REQ 하나에 실을 수 있는 값 조각은 최대 몇 바이트일까?
opcode 1 + 핸들 2 + 오프셋 2.
Read Blob: [Vol 3] Part F §3.4.4.5 · 큐 쓰기: §3.4.6 · 오류 코드(Table 3.4): §3.4.1.1 · GATT의 긴 값 쓰기·신뢰성 있는 쓰기: [Vol 3] Part G §4.9.4·§4.9.5
EATT: 창구 여러 개
EATT(5.2)는 ATT를 10장의 ECFC 채널 여럿 위에 올린다. 순차 규칙은 베어러마다이므로, 채널이 셋이면 요청 셋을 동시에 진행할 수 있다. 덧붙여 바뀌는 것이 있다.
- ATT_MTU는 양쪽 ECFC MTU 중 작은 값이고 최소 64바이트다. MTU 교환 PDU는 쓰지 않는다.
- 알림의 신뢰성: 일반 베어러에서는 버퍼가 넘치면 알림을 버릴 수 있지만, EATT 베어러에서는 받은 알림을 반드시 처리해야 한다. ECFC의 크레딧이 흐름 제어를 대신하기 때문이다.
- 새 PDU: ATT_READ_MULTIPLE_VARIABLE_REQ/RSP(0x20/0x21)는 길이가 다른 값 여러 개를 길이와 함께 한 번에 읽고, ATT_MULTIPLE_HANDLE_VALUE_NTF(0x23)는 알림 여러 개를 PDU 하나에 묶는다.
- EATT 채널은 암호화된 연결 위에서만 연다. 트랜잭션은 한 베어러를 넘나들 수 없다.
EATT 베어러의 ATT_MTU와 채널 요구: [Vol 3] Part G §5.3 · ATT 베어러: [Vol 3] Part F §3.2.11 · 알림 처리 규칙: §3.3.2 · 새 PDU: §3.4.4.11·§3.4.4.12·§3.4.7.4
Database Hash: 데이터베이스의 지문
휴대폰은 한 번 찾은 핸들을 기억해 두고(1권 7장의 캐싱) 다음 연결에서 바로 쓴다. 그런데 펌웨어를 올려 서비스가 바뀌면 옛 핸들이 엉뚱한 특성을 가리킨다. 이를 알아채는 수단이 GATT 서비스의 Database Hash 특성(UUID 0x2B2A)이다. 서버의 서비스 구조를 요약한 128비트 값이라, 클라이언트는 연결할 때 이것만 읽어 보고 기억해 둔 값과 같으면 다시 찾지 않는다.
계산 규칙: 키가 모두 0인 AES-CMAC에, 핸들 순서대로 다음을 이어 붙인 메시지를 넣는다. 서비스·포함·특성 선언과 확장 속성 디스크립터는 (핸들, 타입, 값), 사용자 설명·CCCD·SCCD·표현 형식·집합 형식 디스크립터는 (핸들, 타입)만, 그 밖(특성 값 등)은 넣지 않는다. 모두 리틀 엔디언이다. 사양의 예: 특성 선언 값 {0x02, 0x0005, 0x2A00}은 02 05 00 00 2A로 붙는다. 그래서 장치 이름 같은 특성 값이 바뀌어도 해시는 그대로이고, 특성을 추가하거나 속성(properties)을 바꾸면 해시가 바뀐다.
GATT 데이터베이스와 Database Hash
| 핸들 | 타입 | 값 | 해시에 넣는 바이트 |
|---|
Database Hash 특성(0x2B2A)과 읽는 방법: [Vol 3] Part G §7.3 · 계산 규칙과 예: §7.3.1 · 선언 형식: §3.1·§3.3.1
Robust Caching: change-aware와 0x12
해시와 Service Changed를 둘 다 가진 서버는 Robust Caching을 지원해야 한다. 서버는 연결된 클라이언트마다 “변경을 아는가(change-aware)”를 기억한다. 데이터베이스가 바뀌는 순간 모두 change-unaware가 된다. 본딩한 클라이언트는 연결이 끊긴 사이 바뀌었다면 다시 연결할 때 unaware로 시작한다. Robust Caching을 지원한다고 알린 클라이언트가 unaware인 동안,
- 핸들을 지정한 요청을 보내면 서버는 실행하지 않고 ATT_ERROR_RSP(Database Out Of Sync, 0x12)로 답한다. 베어러마다 한 번만 보낸다.
- 명령은 무시한다. 알림·표시는 Service Changed 표시를 빼고 보내지 않는다.
- Database Hash를 읽고(Read By Type, 핸들 범위 0x0001~0xFFFF) 그다음 요청을 보내거나, 0x12를 받은 뒤 다음 요청을 보내거나, (베어러가 하나라면) Service Changed 표시를 확인하면 change-aware가 된다.
서버가 보는 클라이언트 상태
Robust Caching과 change-aware 전이(그림 2.5~2.7), 0x12를 보내는 조건, 명령 무시: [Vol 3] Part G §2.5.2.1 · 0x12 Database Out Of Sync: [Vol 3] Part F §3.4.1.1
핵심 정리
- opcode = 방법 6비트 + 명령 플래그(비트 6) + 비트 7(6.3에서 이전에 쓰던 값, 0xD2 사라짐). 요청·표시는 베어러마다 하나씩, 30초 넘으면 그 베어러는 끝.
- 긴 값: Read Blob으로 읽고 Prepare/Execute로 쓴다. 되돌아온 조각을 비교하면 신뢰성 있는 쓰기.
- EATT: ECFC 채널 여럿 = 병렬 트랜잭션, ATT_MTU ≥ 64, 알림을 버리지 않음, Read Multiple Variable·Multiple Handle Value NTF.
- Database Hash = AES-CMAC(0, 선언·확장 속성의 핸들·타입·값 + 몇몇 디스크립터의 핸들·타입). 특성 값은 넣지 않는다.
- Robust Caching: unaware 클라이언트의 요청엔 0x12(베어러당 한 번), 명령은 무시. 해시 읽기나 0x12 뒤 다음 요청, SC 확인으로 aware.
확인 퀴즈
opcode 0x52의 뜻은?
ATT 트랜잭션이 30초 안에 끝나지 않으면?
Database Hash 계산에 들어가지 않는 것은?
change-unaware 클라이언트가 쓰기 명령을 보내면?