scanf("%d", int), std::cin >> float, a, b = map(int, readline().split()). 각 C, C++, Pythonsocket: 소켓 객체(file-descripter) 생성send: 연결된 소켓을 통해 데이터 송신recv: 연결된 소켓으로부터 데이터 수신bind, accept: 서버측에서 OS에 소켓 바인딩 요청 및 연결 수락connect: 클라이언트측에서 목적 주소에 연결 요청af: 주소 체계(Address Family), 통신 방식을 지정합니다. 주로 AF_INET(IPv4)나 AF_INET6(IPv6) 혹은 AF_UNIX를 사용합니다.type: 소켓 유형을 지정합니다. 예를 들어, TCP는 SOCK_STREAM, UDP는 SOCK_DGRAM입니다.protocol: 사용할 프로토콜을 지정합니다. 0을 지정하면 type에 따라 자동으로 지정됩니다.int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);socket: 소켓 파일 디스크립터입니다.address: 소켓에 할당할 주소 정보를 담은 구조체입니다. 일반적으로 struct sockaddr_in을 사용하여 IPv4 주소와 포트를 지정합니다.address_len: 주소 구조체의 크기입니다. ipv4 주소인지 ipv6주소인지를 분간하기 위해 사용합니다.backlog: 클라이언트의 연결 요청 큐의 크기를 지정합니다.address: 연결하려는 서버의 주소 정보를 담은 구조체입니다.address: 클라이언트의 주소 정보를 저장할 구조체입니다. 연결이 수락되면 클라이언트의 IP 주소와 포트 정보가 이 구조체에 저장됩니다.address_len: 주소 구조체의 크기를 저장하는 변수입니다. accept 함수가 반환되면 이 변수에는 클라이언트 주소 구조체의 실제 크기가 저장됩니다.buffer: 전송할 데이터가 저장된 버퍼의 포인터입니다.length: 전송할 데이터의 크기(바이트 단위)입니다.flags: 전송 옵션을 지정하는 플래그입니다. 일반적으로 0을 사용하지만, 특정 상황에서는 MSG_DONTWAIT(비동기 전송) 등의 플래그를 사용할 수 있습니다.buffer: 수신한 데이터를 저장할 버퍼의 포인터입니다.length: 수신할 데이터의 최대 크기(바이트 단위)입니다.flags: 수신 옵션을 지정하는 플래그입니다. 일반적으로 0을 사용하지만, 특정 상황에서는 MSG_DONTWAIT(비동기 수신) 등의 플래그를 사용할 수 있습니다.how: 종료 방식입니다. 0은 송신 종료, 1은 수신 종료, 2는 송수신 모두 종료를 의미합니다.서버측: 소켓 생성 -> 포트 바인딩 -> 클라이언트 연결 수락 -> 데이터 송수신 -> 소켓 종료클라이언트측: 소켓 생성 -> 서버 연결 -> 데이터 송수신 -> 소켓 종료recv나 send와 같은 함수들이 블로킹되지 않고 즉시 반환됩니다.int n;
while (n = recv(sock, buf, sizeof(buf), 0)) {
if (n == EWOULDBLOCK || n == EAGAIN) {
continue;
}
if(n > 0){
// process recv
}
}select 콜백 함수를 호출시킵니다.accept, recv, send 함수를 호출합니다.recv/send 연산을 하는 방식으로, 하나의 스레드에서 여러개의 소켓을 감시할 수 있습니다.recv호출 하면서 뒤늦게 IO 연산이 이뤄지는 방식이기에 느리다는 단점이 있습니다.// 약식 코드
FD_SET fdRead;
while(1)
{
// fd_set 초기화
FD_ZERO(&fdRead);
for (var socket in sockets)
FD_SET(*it, &socket);
// fd set에 변화가 발생할 때까지 동기적으로 대기
::select(0, &fdRead, NULL, NULL, NULL);
for(int i=0; i < sockets.length(); ++i)
{
// 비트 검사
if (!FD_ISSET(fdRead.fd_array[i], &fdRead))
continue;
// recv 호출
char szBuffer[1024] = { 0 };
int nReceive = ::recv(fdRead.fd_array[i],
(char*)szBuffer, sizeof(szBuffer), 0);
}
}// pollfd 구조체 배열 준비
struct pollfd fds[MAX_CLIENTS];
// 초기 설정 (루프 밖에서 한 번만 해도 됨)
for (int i = 0; i < MAX_CLIENTS; i++) {
fds[i].fd = client_sockets[i];
fds[i].events = POLLIN; // 읽기 이벤트에 관심 있음!
fds[i].revents = 0; // 결과값 초기화
}
while(1) {
// fd_set을 매번 다시 채울 필요 없이 배열과 개수만 넘김
// 타임아웃 5000ms
int ret = poll(fds, MAX_CLIENTS, 5000);
if (ret <= 0) continue;
for (int i = 0; i < MAX_CLIENTS; i++) {
// 비트 검사 대신 구조체의 revents 필드 확인
if (fds[i].revents & POLLIN) {
// 데이터 수신 (recv)
char buf[1024];
int n = recv(fds[i].fd, buf, sizeof(buf), 0);
// 작업 완료 후 revents는 다음 poll 호출 시 커널이 알아서 초기화함
}
}
}// 1. epoll 인스턴스 생성
int epfd = epoll_create(1);
// 2. 서버 소켓 등록 (최초 1회)
struct epoll_event event;
event.events = EPOLLIN; // 읽기 이벤트 감시
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
// 이벤트가 발생한 소켓들을 담을 배열 준비
struct epoll_event revents[MAX_EVENTS];
while(1) {
// 3. 신호가 온 놈들의 '개수'를 반환하며, revents 배열에 그 명단을 채워줌
int n = epoll_wait(epfd, revents, MAX_EVENTS, -1);
// 4. 전수 조사가 필요 없음! 딱 신호 온 개수(n)만큼만 루프 돌면 됨
for (int i = 0; i < n; i++) {
if (revents[i].data.fd == serv_sock) {
// 신규 연결 처리 (accept)
// 신규 클라이언트도 epoll_ctl로 딱 한 번만 등록해주면 됨
} else {
// 데이터 수신 처리 (recv)
// 명단에 있는 놈들이므로 즉시 작업 수행
}
}
}fetch 함수를 사용해 웹 API를 간편히 사용할 수 있습니다. const response = await fetch(`${API_BASE_URL}/Auth/login`, {
method: "POST",
headers: {"Content-Type": "application/json",},
body: JSON.stringify(data),
});await fetch를 수행하면 알아서 웹 요청을 보낸 컴포넌트가 메시지를 받아서 소비할 수 있는 구조.예를 들어 단순히 수동적으로 돌아가는 서버 측에서는 메시지 꺼내는 스레드가 블로킹이 되어도 문제가 없습니다만, 게임 프레임워크에서는 메인스레드에서 메시지 꺼낼 때 블로킹이되면 게임이 멈춰버리게 돼, 매 루프(tick)가 돌 때 소비할 메시지가 있는지 확인하는 함수를 제공하는 형태로 제작을 해야 합니다.
메시지를 소비해야 할 컴포넌트 객체까지 메시지가 전달되는 구조가 매끄러워야 합니다.
특정 컴포넌트 객체가 서버에 질의를 보낸 후, 날라오는 응답에 의해 능동적으로 반응하는 것처럼 코드를 작성할 수 있는 구조로 만드는게 좋을 것 같습니다.
예를 들면, 웹 프레임워크에서는 각 컴포넌트가 독자적으로 웹 요청(fetch)를 보낸 뒤 그에 대한 응답을 능동적으로 받아서 처리하는 것처럼 돌아가는데,
게임에 적용한 네트워크 라이브러리에서도 그런 구조가 될 수 있도록 하는게 좋을 것 같습니다.