[열혈TCP/IP] 04 TCP 기반 서버 / 클라이언트 1
04 TCP 기반 서버 / 클라이언트 1
인터넷 프로토콜 기반 소켓의 경우 데이터 전송방법에 따라서 TCP 소켓과 UDP 소켓으로 나뉘다. 특히 TCP 소켓은 연결지향형이기 때문에 스트림 기반 소켓
이라고도 이야기한다. TCP는 Transmission Control Protocol의 약자이다.
TCP가 속해있는 TCP/IP 프로코톨 스택
을 먼저 설명한다.
- APPLICATION 계층
- TCP 계층 / UDP 계층
- IP 계층
- LINK 계층
총 네 개의 계층으로 나뉘는데 각 층은 인접한 층과 상호작용을 한다. 데이터 송수신의 과정을 네 개의 영역으로 계층화했다는 의미를 갖느다. 즉 “인터넷 기반의 효율적인 데이터 전송”이라는 문제를 해결하기 위해서 문제를 작게 계층화하는 노력을 한 결과 TCP/IP 프로토콜 스택
이 탄생했다.
OSI 7계층
: 데이터 통신에 사용되는 프로토콜 스택은 총 7계층으로 세분화된다. 하지만 앞서 언급한 4계층으로 이해하고 있어도 시작하는 단계에서는 괜찮은 접근이다.
LINK 계층
이 계층은 물리적인 영역의 표준화의 결과이다. 두 호스트가 인터넷을 통해서 데이터를 주고 받으려면 일단 물리적인 연결이 있어야하며, 이를 표준화한 것이 링크 계층이다.
IP 계층
물리적인 링크 계층이 표준화되면 데이터를 보낼 기본 준비가 된 것이다. 그런데 복잡하게 연결된 인터넷에서 데이터를 전송하기 위해서는 경로를 설정해야한다. 이 문제를 해결하는 것이 IP 계층이고 여기서 사용하는 것이 Internet Protocol, IP
이다. IP 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜이다. 데이터를 전송할 때마다 거쳐야 할 경로를 선택해주지만 일정하지는 않다. 특히 데이터 전송 도중에 경로상에 문제가 발생하면 다른 경로를 선택해주는데, 이 때 데이터가 손실되거나 오류가 발생하는 등 문제가 발생하다고 해서 이를 해결해주지는 않는다.
TCP/UDP 계층
데이터 전송을 위한 경로의 검색을 IP 계층에서 해결해주니 그 경로를 기준으로 데이터를 전송만하면 된다. TCP/UDP 계층은 IP 계층에서 알려준 경로정보를 바탕으로 데이터의 실제 송수신을 담당한다. 때문에 이 계층을 가리켜 전송 계층
이라고 한다.
TCP는 신뢰성 있는 데이터의 전송을 담당하며 TCP가 데이터를 보낼 때 기반이 되는 프로토콜이 IP이다. 이 둘간의 관계는 다음과 같다. IP는 오로지 하나의 데이터 패킷이 전송되는 과정에만 중심을 두고 설계되었다. 따라서 여러 개의 데이터 패킷을 전송한다 하더라도 각각의 패킷이 전송되는 과정은 IP에 의해서 진행되므로 전송의 순서는 물론, 전송 그 자체를 신뢰할 수 없다. 만약 패킷 1,2,3을 전송할 때 도착 순서와 3개 패킷 모두 도착할 것이라는 것을 보장하지 못한다.
하지만 TCP 프로토콜이 추가되면서 데이터를 송수신하면 “어떤 패킷까지 수신했는지” 정보 교환을 통해서 모든 패킷이 완벽하게 도착하는 것을 보장한다. 제대로 도착하지 못하면 재전송을 한다. 패킷 A를 보냈는데 A를 받았다는 말이 없어서 time out이 걸리면 패킷 A를 다시 보낸다.
APPLICATINO 계층
위 내용은 소켓을 생성하면 데이터 송수신 과정에서 자동으로 처리되는 것이다. 데이터 전송경로를 확인하는 과정과 데이터 수신에 대한 응답의 과정이 소켓이라는 것 하나에 감춰져서 알아서 진행된다. 이제 소켓이라는 도구가 주어지고, 개발자는 이를 사용해서 무엇인가를 만들면 된다. 이렇게 무엇인가를 만드는 과정에서 프로그램 성격에 따라서 클라와 서버 간의 데이터 송수신에 대한 규칙들이 정해지기 마련인데, 이를 가리켜서 APPLICATION 프로토콜이라고 한다. 그리고 대부분의 네트워크 프로그래밍은 APPLICATION 프로토콜의 설계 및 구현이 상당부분을 차지한다.
04-2 TCP기반 서버 클라이언트 구현
아래는 TCP 서버 구현을 위한 기본적인 함수의 호추 순서를 나타낸다.
- socket() : 소켓 생성
- bind() : 소켓에 주소 할당
- listen() : 연결요청 대기상태. 서버의 listen 이후 클라가 connect()를 할 수 있다.
- 클라이언트의 연결요청도 인터넷을 통해서 들어오는 일종의 데이터 이므로 당연히 소켓이 있어야 받을 수 있다. 서버 소켓의 역할을 연결 요청을 받아들이는 것이라고 할 수 있다.
- accept()
- listen 호출 이후 클라의 연결 요청이 들어오면, 들어온 순서대로 연결요청을 수락해야 한다.
- 연결 요청을 수락한다는 것은 데이터를 주고 받을 수 있는 상태가 됨을 의미한다.
- 연결 요청을 받아들이는 소켓이, 연결 대기 중인 연결요청 중 먼저의 것을 가져와서 데이터를 송수신하기 위한 새로운 소켓을 만들고 이의 파일 디스크립터를 반환한다.
- 이 때 연결요청을 한 클라 소켓과 연결까지 끝낸 상태로 fd를 반환한다.
- 클라의 connect() 이전에 accept()가 호출되면 queue는 비어있기 때문에 block이 걸리고 연결 요청을 기다린다.
- read() / write()
- close()
아래는 클라이언트의 기본적인 함수 호출 순서를 나타낸다.
- socket()
- connect()
- 연결 요청을 하면 연결 요청이 접수되거나 중단된다.
- 연결 요청이 접수된 상태는 accept()가 호출된 상태가 아니라 queue에 연결 요청이 저장된 상태이다.
- 따라서 connect() 함수가 반환되어도 바로 서비스가 실행되지는 않을 수 있다.
- 클라 소켓은 bind()를 호출하지 않아도 connect()를 호출할 때 커널에 의해서 호스트에 할당된 IP와 임의의 port가 할당된다.
- read() / write()
- close()
04-3 Iterative 기반의 서버, 클라 구현
클라의 메시지를 서버가 다시 전송하는 에코 서버를 만들어보자.
- socket()
- bind()
- listen()
- accept()
- read() /write()
- close(client) 후 다시 accept()로 loop
- close(server)
에코 서버의 설계는 다음과 같다.
- 서버는 한 순간에 하나의 클라이언트와 연결되어 에코 서비스를 제공한다.
- 서버는 총 다섯 개의 클라이언트에게 순차적으로 서비스를 제공하고 종료한다.
- 클라는 프로그램 사용자로부터 문자열 데이터를 입력 받아서 서버에 전송한다.
- 서버는 전송 받은 문자열 데이터를 클라에게 재전송한다.
- 서버와 클라간의 문자열 에코는 클라가 Q를 입력할 때까지 계속한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//echo_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
char message[BUF_SIZE];
int str_len, i;
struct sockaddr_in serv_adr;
struct sockaddr_in clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock=socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock, 5)==-1)
error_handling("listen() error");
clnt_adr_sz=sizeof(clnt_adr);
for(i=0; i<5; i++)
{
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
if(clnt_sock==-1)
error_handling("accept() error");
else
printf("Connected client %d \n", i+1);
while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//echo_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
위 예제는 잘 동작하지만, TCP는 데이터의 경계가 존재하지 않는 특성을 갖는다. 즉, read()와 write()의 호출 횟수가 일치하지 않을 수 있다. 즉, 둘 이상의 write()로 입력된 다수의 문장을 한 번에 read()에서 둘 다 echo하는 문제점을 야기할 수 있다.
이 문제의 해결은 chaper 5에서 다룬다.
Leave a comment