클라이언트 프로그램#
클라이언트 프로그램은 1. 소켓 생성, 2. 서버의 인터넷 주소 설정, 3. 연결 요청, 4. 통신(읽기/쓰기), 5. 연결 종료 순서로 진행된다.
소켓 생성#
1
2
3
4
5
6
7
8
9
10
11
12
13
| #include <sys/types.h>
#include <sys/socket.h>
int main() {
...
int ssock;
if((ssock # socket(PF_INET, SOCK_STREAN, IPPROTO_TCP)) < 0) {
perror("socket error : ");
exit(1);
}
...
}
|
- 소켓을 생성한다. socket이라는 함수에 인자값을 주어 소켓 생성에 성공하면, 0보다 큰값의 디스크립션을 반환한다. 보통 3 이상의 값이 반환된다. 0은 표준입력, 1은 표준출력, 2는 표준에러로 예약되어 있다. 소켓 생성에 실패하면 음수값의 디스크립션을 반환한다.
- socket 함수
- 소켓 지정자 socket(인자 도메인, 소켓의 형태, 프로토콜의 종류)
- 해당 소켓을 생성하고 소켓의 디스크립션을 반환한다. 즉, 인자 값을 받아 소켓을 생성한 후에 해당 소켓의 지정자를 반환한다.
1
2
3
4
| #include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol)
|
- 인자 도메인 (domain)
- 네트워크의 종류를 나타낸다. (노벨IPX, ATM, X.25, IPv6, TCP/IP 등)
- 네트워크의 종류에 따라 프로토콜이 다르게 구성되어 있으며, 리눅스에서는 도메인이라는 개념을 사용해 구분한다.
- TCP/IP는 PF_INET 프로토콜 패밀리에 정의되어 있다. PF_INET은 주소값이다. IP주소를 사용한다.
- 프로토콜 패밀리는 <sys/socket.h> 에서 정의한다.
- 로컬 소켓을 사용해 소켓을 내부 프로세스가 통신하는 용도로 사용하려면 PF_UNIX나 PF_LOCAL를 사용한다. 내부 프로세스 통신을 위해 사용할 때는 데이터 전송을 위한 파일 경로가 들어가게 된다.
프로토콜 패밀리 | 설명 |
---|
PF_UNIX | 유닉스 도메인 소켓 |
PF_LOCAL | PF_UNIX 소켓의 포직스 표현 |
PF_INET | 인터넷 프로토콜(TCP/IP) |
PF_IPX | 노벨 IPX |
PF_AX25 | 아마추어 라디오 AX.25 |
PF_APPLETALK | 애플토크 DDP |
PF_INET6 | IPv6 프로토콜 |
PF_ROSE | 아마추어 라디오 X.25 PLP |
PF_X25 | X.25 프로젝트를 위해 예약 |
PF_IRDA | 적외선(IrDA) 소켓 |
PF_NETBEUI | 802.2LLC 프로젝트를 위해 예약 |
PF_LLC | 리눅스 LLC |
PF_BLUETOOTH | 블루투스 소켓 |
- 소켓의 형태 (type)
- 소켓의 형태는 소켓의 연결 형식을 정의한다.
- PF_INET에서 주로 사용하는 소켓 연결 형식에는 SOCK_STREAM, SOCK_DGRAM, SOCK_RAW가 있다.
- SOCK_STREAM : TCP를 통해 전송되는 신뢰성 있는 연결
- SOCK_DGRAM : UDP를 통해 전송되는 신뢰성 없는 연결.
- SOCK_RAW : 헤더를 직접 만들어서 사용해야 하는 연결 형식. SOCK_STREAM, SOCK_DGRAM의 헤더는 운영체제에서 자동으로 만들어준다.
- 프로토콜의 종류 (protocol)
- 소켓에서 사용될 프로토콜을 지정한다.
프로토콜 | 설명 |
---|
0 | 도메인과 소켓의 형태에 따른 기본 프로토콜이 자동 결정된다 |
IPPROTO_IP | |
IPPROTO_TCP | TCP 프로토콜 사용 |
IPPROTO_UDP | UDP 프로토콜 사용 |
IPPROTO_RAW | |
IPPROTO_ICMP | |
IPPROTO_ICMPV6 | |
서버의 인터넷 주소 설정#
1
2
3
4
5
6
7
8
| ...
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family # AF_INET; /* # PF_INET */
server_addr.sin_addr.s_addr # inet_addr("127.0.0.1");
server_addr.sin_port # htons(2217);
...
|
- 접속할 서버의 주소를 설정한다. 프로토콜 패밀리와 IP 주소, Port 주소를 각각 정해준다. 네트워크 바이트 오더를 적용해야 하기 때문에 IP 주소와 Port 주소는 inet_addr(), htons() 함수를 통해서 값을 넣어주어야 한다.
- sockaddr_in 구조체는 INET 인터넷 프로토콜의 TCP 헤더를 구성한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| typedef unsigned short sa_family_t;
struct in_addr {
__u32 s_addr;
};
#define __SOCK_SIZE__ 16
struct sockaddr_in {
sa_family_t sin_family; /*주소패밀리*/
unsigned short int sin_port; /*포트번호*/
struct in_addr sin_addr; /*IP주소*/
unsigned char __pad[__SOCK_SIZE - sizeof(short int) - sizeof(unsigned short int) - sizeof(struct in_addr)];
/* Pad to size of 'struct sockaddr' */
};
|
- inet_addr 함수
- 이진 바이너리 형식의 IP 주소 inet_addr(문자 형식의 IP 주소)
- 문자 형식의 IP 주소를 네트워크 바이트 오더를 적용한 이진 바이너리 코드로 바꾸어 변환한다.
- 리틀 엔디안(Little Endian)과 빅 엔디안(Big Endian) 이라는 바이트 오더의 개념을 알아야 한다. 네트워크 프로토콜 표준에서 사용하는 바이트 오더는 빅 엔디안이다. x86 호환 아키텍처에서는 리틀 엔디안을 사용한다.
1
2
3
4
5
| #include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long int inet_addr(const char *cp)
|
- htons 함수
- 네트워크 바이트 오더가 적용된 이진 바이너리 16비트 값 htons (16비트 변수 값)
- 16비트의 unsigned short형 숫자 값에 네트워크 바이트 오더를 적용한 후 반환한다.
1
2
3
| #include <netinet/in.h>
uint16_t htons(uint16_t hostshort)
|
연결 요청#
1
2
3
4
5
6
7
| ...
clen # sizeof(server_addr);
if(connect(ssock, (struct sockaddr *)&server_addr, clen) < 0) {
peror("connect error :");
exit(1);
}
|
- 소켓 생성이 완료되면 생성된 소켓을 이용하여 서버에 연결해야 한다. 이때 connect() 함수를 사용한다.
- connect 함수
- 리턴 값 connect(소켓 디스크립션, 서버의 IP 주소와 포트번호, 길이)
- 소켓 연결을 시작한다. 생성된 소켓과 서버의 정보를 이용해서 해당 서버에 연결한 후 결과값을 리턴한다. 1이면 성공, -1이면 실패를 의미한다.
1
2
3
4
| #include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, soclen_t addrlen);
|
통신(읽기/쓰기)#
1
2
3
4
5
6
| memset(buf, 0, MAXBUF);
if(read(ssock, buf, MAXBUF) <# 0) {
perror("read error :");
exit(1);
}
|
- 서버에서 데이터를 읽어오려면 read() 함수를 사용한다. 데이터를 수신한 후 buf에 저장한다.
- read 함수
- 읽은 값의 길이 read(파일 디스크립터, 읽은 데이터를 저장할 버퍼, 읽을 데이터 최대 길이)
- 지정된 파일 디스크립터에서 데이터를 읽어서 지정된 버퍼에 데이터를 넣은 후 읽은 데이터의 길이를 반환한다. 오류가 발생하면 -1을 반환한다. 네트워크 프로그래밍에서 파일 디스크립터에 해당하는 것이 소켓 디스크립터이다.
1
2
3
| #include <unistd.h>
ssize_t read(int fd, void *buf, size_t count)
|
연결 종료#
- close 함수
- 성공 여부 반환 close(파일 디스크립터)
- close 함수는 파일 디스크립터를 닫는다. 닫힌 파일 디스크립터는 더 이상 참조되거나 사용될 수 없다. 닫기에 성공하면 0을 반환하고 오류가 발생하면 -1을 반환한다.
1
2
3
| #include <unistd.h>
int close(int fd)
|
서버 프로그램#
서버 프로그램은 1. 소켓 생성, 2. 인터넷 주소 부여, 3. 연결 수신 대기, 4. 연결 수신, 5. 통신(읽기/쓰기), 6. 연결 수신 대기(이후 과정 반복) 순서로 진행된다.
소켓 생성#
클라이언트 프로그램 부분과 같다.
인터넷 주소 부여#
1
2
3
4
5
6
7
8
9
| ...
server_addr.sin_family # AF_INET;
server_addr.sin_addr.s_addr # htonl(INADDR_ANY);
server_addr.sin_port # htons(2999);
if(bind(ssock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 {
perror("bind error : ");
exit(1);
}
|
- server_addr.sin_addr.s_addr 에 INADDR_ANY 값을 넣었다. 이는 특정 주소를 넣으면 특정 클라이언트만 접속되기 때문이다. 모든 클라이언트가 접속 할 수 있도록 하기 위해서는 INADDR_ANY 값을 넣어주면 된다.
- htonl 함수는 htons 함수와 마찬가지로 네트워크 바이트 오더를 적용하기 위한 것이다. htons는 short형 숫자를 반환하고, htonl은 long형 숫자를 반환한다.
- htonl 함수
- 32비트 네트워크 바이트 오더형 숫자 htonl(32비트 호스트 오더형 숫자)
- 32비트의 호스트 오더형 숫자를 네트워크 바이트 오더의 숫자로 변경한다. 인텔 CPU에서는 리틀 엔디안인 32비트 숫자 변수를 네트워크 바이트 오더인 빅 엔디안을 적용한 후 반환한다.
1
2
3
| #include <netinet/in.h>
uint32_t htonl(uint32_t hostlong)
|
- struct sockaddr_in 구조체에 주소 등의 옵션을 설정했으면 이것을 생성된 소켓에 적용시켜야 한다. 이때 bind 함수를 사용한다.
- bind 함수
- 성공 여부 반환 bind(소켓 디스크립터, 로컬 주소, 로컬 주소 구조체 크기)
- 생성된 소켓에 로컬 주소를 할당한다. 로컬 주소에 설정할 수 있는 옵션은 연결될 클라이언트의 주소 패밀리, IP 정보, 포트 정보가 있다. 성공하면 0을 반환, 오류가 발생하면 -1를 반환하면서 errno에 오류 번호를 넣는다.
1
2
3
4
| #include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr, socklen_t adrlen)
|
연결 수신 대기#
1
2
3
| if(listen(ssock, 5) < 0) {
perror("listen error : ");
exit(1);
|
- 연결 수신을 대기하도록 하려면 listen 함수를 사용한다. listen 함수에 연결하고 싶은 소켓 디스크립터와 리눅스 커널의 큐에 저장할 수 있는 최대 접속 수를 설정한다.
- listen 함수
- 성공 여부 반환 listen(소켓 디스크립터, 최대 연결할 큐의 길이)
- 해당 소켓에서 연결을 기다린다. 연결은 큐의 길이만큼만 가능하다. 성공하면 0, 오류가 발생하면 -1를 반환하면서 errno에 오류 번호를 넣는다.
1
2
3
| #include <sys/socket.h>
int listen(int s, int backlog)
|
연결 수신#
1
2
3
4
5
6
| ...
if((csock # accept(ssock, (struct sockaddr *)&client_addr, &clen)) < 0) {
perror("accept error : ");
close(ssock);
exit(1);
}
|
- 소켓의 연결을 받아들이려면 accept 함수를 사용한다. accept 함수는 소켓 디스크립터를 이용해 연결 요청을 받아들인다. 연결 요청이 없는 경우에 accept 함수를 사용하면 연결 요청이 들어올 때까지 대기 상태에서 계속 연결 요청 여부를 확인하게 된다.
- accept 함수
- 성공한 소켓 디스크립터 accept(소켓 디스크립터, 접속한 상대편 연결 정보, 연결 정보 길이)
- 해당 소켓에 연결 요청이 들어왔을 때 연결을 받아들인다. 연결 요청이 없는 경우에는 연결 요청이 발생할 때까지 대기 상태에서 해당 소켓을 계속 감시한다. 오류가 발생하면 -1을 반환하고 연결에 성공하면 해당 소켓 디스크립터인 음이 아닌 정수를 반환한다.
1
2
3
4
| #include <sys/types.h>
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen)
|
통신(읽기/쓰기)#
1
2
| if(write(csock, buf, MAXBUF) <# 0)
perror("write error : ");
|
- 데이터를 전송하려면 write 함수를 사용한다.
- write 함수
- 성공 시 쓰여진 데이터 길이 write(파일 디스크립터, 전송할 버퍼, 전송할 데이터 길이)
- 버퍼에서 데이터를 읽어서 파일 디스크립터에 인수로 주어진 크기만큼 쓴다. 쓰기게 성공하면 쓰기에 성공한 길이를 반환하고 오류가 발생하면 -1을 반환하면서 errno에 오류 번호를 넣는다.
1
2
3
| #include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count)
|
UDP 서버/클라이언트 프로그래밍#
다중 접속 처리 서버 구현#
RAW 소켓을 이용한 패킷 제어#
참고 문서#
참고 문헌#
- 백창우, 최영호, 조경민, 윤경훈, 윤상배. 『TCP/IP 소켓 프로그래밍』, 한빛미디어, 2005.