C socket 통신
처음 회사에 가자마자 들은 이야기이다.
멋진 개발자는 low level 이 탄탄해야 한다. 우리 가상화 제품은 소켓을 잘알아야 한다. 소켓 프로그래밍을 C로 구현해보자!
처음 할때는 뭐가뭔지 하나도 모르겠었고, 네트워크 기본 개념이 이렇게 쓰이는구나 싶었다. 또, 내가 사용하던 함수들의 안쪽은 이렇게 구현이 되어있구나 싶었다.
소켓의 정의부터 천천히 정리해보자. 소켓은 네트워크 상에서 양방향 통신을 하기 위한 양쪽의 엔드포인트 이다.
인터넷상에서 특정 PC 를 식별하기 위한 최소 단위는 무엇인가? 복잡한 개념을 빼고 생각하면 일단은 IP 라고 할수 있을 것이다. 그렇다면 한 노드가 다른 노드의 IP 로 정보를 준다면 잘 받아들일까?
이 통신의 용도를 모르므로 이게 어떤 통신이며 나의 어떤 요청에 의한 어떤 통신임을 파악하는데 애를 먹을 것이다. 따라서 소켓의 주소는 IP:포트 형식으로 이루어져 있다.
IP 에 대한 이야기를 내부망, 외부망, 게이트웨이, NAT 이란 개념도 있지만 이는 설명이 길어지기 때문에 링크로 대체하겠다.
https://yeon-lee.tistory.com/179 https://yeon-lee.tistory.com/71
소켓 기본 개념
기본적인 인터넷 소켓 사용 방법은 다음과 같다.
소켓은 소켓 객체 생성 > 포트 bind > listen 상태로 서버가 들어가면 요청을 들을 준비를 한 것이다
이후 클라이언트가 해당 IP/포트 (소켓주소) 에 connect 하면 send/recv 로 쌍방향 소통이 가능해진다.
이렇게만 들으면 전혀 감이 안올 것이기 때문에 내가 정리한 #1 #2 #3 내용을 코드와 함께 보면 이해가 쉬울 것이다
https://yeon-lee.tistory.com/70 https://yeon-lee.tistory.com/79 https://yeon-lee.tistory.com/90
[네트워크] [C 소켓통신 #1] codelite 이용한 소켓통신 (send, receive flag 의미)
미션 1. 고객의 요청을 대기중인 서버를 만들어보자 (hint. 고객의 요청을 복사하는 서버/고객 코드) https://koyo.kr/post/c-socket-example/ 이 프로젝트를 따라해보았당 block 기다리는 행위. 어디까지 진행
yeon-lee.tistory.com
그리고 함수가 익숙하지 않다면 간략한 함수와 툴 내용을 정리해보았으니 찾아서 보면 편할 것이다
https://yeon-lee.tistory.com/90 https://yeon-lee.tistory.com/101
버퍼 크기 제한
buffer 크기를 6byte 로 제한하고 이보다 큰 메시지를 어떻게 보낼지를 고민해보았다.
memset(buf, 0, BUF_LEN);
memset(msg, 0, MSG_MAX);
fflush(stdout);
while(getchar() != '\\n');
while (1){
fputs("메시지를 입력하세요 : ", stdout);
fflush(stdout);
do {
fgets(msg, MSG_MAX, stdin);
r = send(client_sock, msg, strlen(msg), 0);
} while (msg[r-1] != '\\n');
}
client 에서 메시지를 보내는 코드는 다음과 같다
while (1) {
memset(buf, 0, BUF_LEN);
recv_len = recv(client_sock, buf, BUF_LEN, 0);
if (recv_len < 0) { printf("client close\\n"); break; }
else {
if (recv_len < BUF_LEN){
tmp_char = buf[recv_len - 1];
buf[recv_len - 1] = '\\0';
printf("msg : %s%c", buf, tmp_char);
}
else if (recv_len == BUF_LEN) {
tmp_char = buf[recv_len - 1];
buf[recv_len - 1] = '\\0';
printf("msg : %s%c", buf, tmp_char);
while (tmp_char != '\\n') {
recv_len = recv(client_sock, buf, BUF_LEN, 0);
for (int i = recv_len; i < BUF_LEN; i++){
buf[i] = '\\0';
}
tmp_char = buf[recv_len-1];
buf[recv_len-1] = '\\0';
printf("%s%c", buf, tmp_char);
}
}
}
}
처음 짠 server 에서 응답을 받는 코드는 다음과 같았다. 응답 string 의 마지막이 /0으로 온다는 것에 집착해서 가독성과 중복이 아주 많다.
끝 문자열을 항상 잘라서 tmp으로 보관하고 /0 을 임의로 지정해서 넣는 이유는 배운 secure coding 을 실현하기 위해서였다.
BUF_LEN 버퍼 길이를 6byte로 설정해 client 에서 123456 을 보낼 경우 받아지는 값은 12345\0, 6\n\0 이 받아진다
첫번째 else 문에 BUF_LEN 이하와 동일할 경우에만 짜져 있는 이유는 4열의 recv() 함수가 BUF_LEN 으로 잘라주기 때문이다. 만일 recv_len == BUF_LEN 일 경우에는 중복해서 계속 받아야 한다.
끝 문자열이 /n 으로 조건을 준 이유는 client 에서 123 을 보낼 경우 server 단에서는 123/n/0 으로 받아지기 때문에 맨 끝열은 항상 /n 으로 주어진다.
그리고 fgets 로 버퍼 크기를 제한 하면서 buf clean 을 여러 방식으로 진행할 수 있었는데 그 고민이 녹아있는 링크가 이거다.
while (1) {
is_first = 1;
ch = 0;
while (ch != '\\n') {
memset(buf, 0, BUF_LEN);
recv_len = recv(client_sock, buf, BUF_LEN, 0);
if (recv_len < 0) { printf("client close\\n"); break; }
ch = buf[recv_len - 1];
buf[recv_len - 1] = 0;
if (is_first) { printf("msg : "); is_first = 0; }
printf("%s%c", buf, ch == '\\n' ? '\\n' : ch);
}
}
리팩토링한 코드는 다음과 같다
끝항이 \n이기만을 한걸 잡아내면 되기 때문에 삼항연산자로 간결하게 표현할 수 있었다
클라이언트에서 https 웹서버로 curl 로 요청 보내기
우리가 사용하는 네이버, 구글 모두 이 원리를 통해서 통신을 한다.
우린 https://www.naver.com/admin/… 이라는 주소로 서버에게 http request 를 보내고 http response 가 client 에게 온다.
response된 http header와 body 구조로 되어 있고, body 에 담긴 html 은 chrome, firefox 같은 웹브라우저가 파싱해서 우리가 보는 화면으로 띄워주는 개념이다.
HTTP 구조 : https://yeon-lee.tistory.com/87 https://yeon-lee.tistory.com/85
sprintf(message, "GET %s HTTP/1.1\\nHost: %s\\nUser-Agent: curl/7.68.0\\nAccept: */*\\n\\n", path_p, domain_p);
아까까지는 server 에 요청을 보냈지만 이번에는 네이버에게 요청을 보내보기로 했다
curl 은 http, https 프로토콜로 웹서버에게 요청을 보내고 응답을 받을 수 있는 명령줄 유틸리티이다.
https://www.naver.com 이라는 input 을 넣으면 domain 을 파싱해서 inet_ntoa DNS 함수를 거쳐 IP 를 뽑아내고 이를 http request 형식에 맞추어 보내면 response 를 받을 수 있다.
소켓을 이용해서 카카오톡 단체톡 기능 구현
서버와 클라이언트가 연결되면 클라이언트는 소켓 ID 를 부여받는다.
그 ID 를 server 에서 list 로 저장을 해 둔다. 서버의 메인스레드는 receiver 역할을 하고, 자식 스레드를 연결된 숫자의 client 만큼 만들어 sender 역할을 한다
while(cnt < CLIENT_NUM_MAX) {
struct client_context cl_ct;
client_sock = accept(server_sock, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
_server_management.socks[cnt] = client_sock;
cl_ct.management = &_server_management;
cl_ct.sock = client_sock;
if (client_sock < 0) { perror("accept failed"); break; }
r = pthread_create(&thread_id[cnt], NULL, server_proc_entry, (void *)cl_ct.sock);
if (r < 0) { perror("thread create failed"); break; }
cnt++;
}
생성된 자식 스레드는 그 연결이 종료될 경우에 join 을 한다
for (i = 0; i < CLIENT_NUM_MAX; i++){
pthread_join(thread_id[i], NULL);
}
해당 연결된 socket 에서부터 메시지가 들어오면 for 문을 돌면서 서버의 소켓 ID 리스트에 존재하는 모든 소켓에 받은 메시지를 자식 스레드에서 보내준다.
for (i = 0; i < CLIENT_NUM_MAX; i++){
if (_server_management.socks[i] != _server_management.socks[cnt]) {
if (is_first) send(_server_management.socks[i], init_msg, strlen(init_msg), 0);
send(_server_management.socks[i], buf, strlen(buf), 0);
}
}
소켓을 이용해서 카카오톡 개인톡 기능 구현
위와 동일하게 서버와 클라이언트가 연결되면 소켓 주소를 기반으로 한 소켓 ID 클라이언트, 서버가 저장한다.
여기서도 위와 동일하게 서버와 연결된 client 숫자만큼의 responser 역할을 하는 자식 스레드가 만들어진다
while(cnt < CLIENT_NUM_MAX) {
struct client_context cl_ct;
client_sock = accept(server_sock, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen);
_server_management.socks[cnt] = client_sock;
cl_ct.management = &_server_management;
cl_ct.sock = client_sock;
if (client_sock < 0) { perror("accept failed"); break; }
r = pthread_create(&thread_id[cnt], NULL, server_proc_entry, (void *)cl_ct.sock);
if (r < 0) { perror("thread create failed"); break; }
cnt++;
}
클라이언트는 연결하고 싶은 ID 를 입력하면 서버에서 삼항연산자로 소켓 ID 가 교환되어 상호 개인 톡방을 만들 수 있다.
r = send(sock_from_outside == _server_management.socks[0] ? _server_management.socks[1] : _server_management.socks[0], buf, strlen(buf), 0);
이런식으로 list 에 저장된 socket [0] 과 [1] 이 server 를 거쳐서 개인톡방을 만들 수 있다
소켓이 응용계층에서 일어날까?
엄밀한 의미의 소켓은 객체간의 end-to-end 를 이어주는 정보 전달의 파이프 개념에 가깝다.
따라서 우리가 pc 에 usb 를 꽂을때도 usb 소켓이라는 이야기를 한다.
우리가 지금까지 쓴 소켓 주소가 ip:포트 로 구성되어 있다는 이야기를 들었을 것이다.
하지만 이는 정확한 의미의 소켓이라고 보기는 힘들고, 인터넷 소켓이라고 보는 것이 더 정확하다.
다시 말해 응용계층에서 웹서버와 웹브라우저 간에 통신하기 위해서 TCP 기반의 전송계층의 툴을 갖다 쓰는것에 가깝다.
우리가 아는 http 소켓과 달리 unix_socket 의 경우 로컬 프로세스 간에 통신을 주로 하고 TCP 기반이 아니라 stream 형식이 아닌 datagram 형식으로 패킷을 전송할 수도 있다.
리눅스 닥스에 있는 매뉴얼의 c의 sockaddr_un, sockaddr_in 을 보면 좋은 예가 될 수 있다.
# internet socket
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
};
# unix socket
struct sockaddr_in {
short sin_family; // e.g. AF_INET
unsigned short sin_port; // e.g. htons(3490)
struct in_addr sin_addr; // see struct in_addr, below
char sin_zero[8]; // zero this if you want to
};
https://man7.org/linux/man-pages/man3/sockaddr.3type.html 더 자세한 내용은 해당 링크에 있다.
유닉스소켓의 경우 path 가 파일디렉토리로 설정을 한다.
인터넷에서 파일을 주고받는게 아닌 로컬 프로세스 간에 파일을 주고 받는데에 쓰이기 때문에 주소가 ip:포트 일 필요가 없는 것이다.
이를 보면 우리가 지금까지 쓴 소켓은 인터넷 소켓에 국한된다는 사실을 알 수 있다.
'개발 > C' 카테고리의 다른 글
[C] inline (0) | 2023.02.23 |
---|---|
[C] 지역변수 전역변수 쓰레드 공유 (0) | 2023.02.09 |
[C] 상속 구현, 버전 구현 (0) | 2023.01.20 |
[C] syscall이란 (0) | 2023.01.13 |
[C] pthread_create, pthread_join, pthread_exit, fork (0) | 2023.01.13 |