페이지의 컨텐츠를 보아하니, 따로 페이지를 뽑아내도 될것 같아 문서구조조정 하였습니다. 원래 페이지 이름은 데블스캠프2005/Socket Programming in Unix/Windows Implementation였습니다. - 임인택
주제 : Socket Programming의 기초적인 부분을 알아본다.
Contents
1.1. Socket ¶
※ 소켓이란?
~cpp #include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol); // 성공 시 파일 디스크립터, 실패 시 -1 리턴
1.1.2. type: 서비스 타입 ¶
※ TCP/IP, UDP란?
SOCK_STREAM : 스트림 방식의 소켓 생성 (TCP/IP)
1.1.3. protocol: 프로토콜 ¶
IPPROTO_TCP : TCP 기반. 값은 0이다.
IPPROTO_UDP : UDP 기반. 값은 0이다.
// 우리가 사용하는 프로토콜인 TCP, UDP가 0이므로 0으로 써도 무방하다.
// 구체적인 프로토콜을 선택할 때 사용하는데 대부분의 응용 프로그렘에서는 0으로 지정하면 된다.
// 우리가 사용하는 프로토콜인 TCP, UDP가 0이므로 0으로 써도 무방하다.
// 구체적인 프로토콜을 선택할 때 사용하는데 대부분의 응용 프로그렘에서는 0으로 지정하면 된다.
~cpp 예제) main(){ int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if(sockfd == -1) fprintf(stderr, "socket 함수에서 에러"), exit(1); // 에러가 났을경우( sockfd == -1) 에러를 출력하고 프로그램 종료. }
1.2. 네트워크 정보 ¶
~cpp struct sockaddr { u_short sa_family; /* address family */ char sa_data[14]; /* 주소(IP 주소 + 포트 번호) */ };
~cpp struct sockaddr_in { short sin_family; // 주소 체계를 나타낸다. u_short sin_port; // port 번호 struct in_addr sin_addr; // ip 주소 char sin_zero[8]; // 쓰지 않는 주소 };
※ 왜 sockaddr과 sockaddr_in의 structure가 같을까?
1.2.1. sin_family: // 주소체계 ¶
AF_INET : 인터넷 주소 체계
AF_UNIX : 유닉스 파일 주소 체계
AF_NS XEROX : 주소 체계
// sockaddr_in 은 TCP/IP체제 이므로 AF_INET만 사용한다. -> TCP/IP는 인터넷 기반이므로.
// AF_INET/PF_INET -> 인터넷 프로토콜 체계 사용.
// AF_INET/PF_INET -> 인터넷 프로토콜 체계 사용.
~cpp 데이터를 Big-Endian으로 변환 시켜주는 체계. unsigned short integer 변환 (2바이트 크기) htons(): host-to-network 바이트 변환 (Big-Endian으로 변환) ntohs(): network-to-host 바이트 변환 (해당 시스템) unsigned long integer 변환 (4바이트 크기) htonl(): host-to-network 바이트 변환 (Big-Endian으로 변환) ntohl(): network-to-host 바이트 변환 (해당 시스템) ※ 왜 우리는 데이터를 Big-Endian으로 변환 시켜주어야할까? ※ 그렇다면 우리가 전송하는 데이터 모두 Big-Endian으로 변환 시켜주어야할까?
1.2.2. sin_port: ¶
NULL : 임의의 포트를 할당한다. client에서 사용한다.
// u_short sin_port 은 Big-Endian을 사용한다.
// 따라서 Little_Endian을 사용하는 시스템에서는 Big-Endian으로 바꿔줘야한다.
// u_short sin_port 은 Big-Endian을 사용한다.
// 따라서 Little_Endian을 사용하는 시스템에서는 Big-Endian으로 바꿔줘야한다.
1.2.3. sin_addr: ¶
INADDR_ANY : 자기 자신의 주소를 할당한다. (== 0)
// sin_addr은 인터넷 주소를 담고 있으므로 4 바이트가 필요하다.
cf. 도메인 네임을 통한 연결은 설명하지 않겠습니다.
// sin_addr은 인터넷 주소를 담고 있으므로 4 바이트가 필요하다.
cf. 도메인 네임을 통한 연결은 설명하지 않겠습니다.
~cpp struct in_addr { unsigned long s_addr; };
~cpp /* inet_addr(): 주소를 long형으로 계산하고 htonl()를 사용해 Big-Endian으로 변환 후 값을 return 한다. // 165.194.27.129 -> 165*256*256*256 + 194*256*256 + 27*256 + 129 = 2780961665 // 2780961665 의 값은 Little-Endian 체계에서는 811BC2A5이다. // 이것을 A5C21B81로 바꿔 저장한다. 예제 ) ina.sin_addr.s_addr = inet_addr("127.0.0.1"); */
1.2.4. sin_zero''''''8'''''': ¶
// sin_zero 배열은 항상 0으로 채워져 있어야한다.
※ 왜 sin_zero가 만들어졌을까요?
※ 왜 sin_zero가 만들어졌을까요?
~cpp 예제) #define PORT 9999 #define SERVER_IP "165.194.27.129" main(){ struct sockaddr_in ina; memset((struct sockaddr *)&ina, 0, sizeof(struct sockaddr)); // sin_zero를 0으로 채운다. // bzero라는 함수도 있지만 초기에 0으로 채우는 것이 편하다. // bzero(&(ina.sin_zero), 8); ina.sin_port = htons(PORT); // PORT의 경우 정수를 넣어야한다. ina.sin_addr.s_addr = inet_addr(SERVER_IP); // 클라이언트의 경우 // SERVER_IP의 경우 문자열 포인터를 넣어야한다. // 165.194.27.129 -> 165*256*256*256 + 194*256*256 + 27*256 + 129 = 2780961665 로 저장이 된다. // ina.sin_addr.s_addr = INADDR_ANY; // 서버의 경우 }
2.1. Bind - socket과 네트워크 정보를 연결하는 Bind!!! ¶
~cpp #include <sys/socket.h> int bind(int sockfd, struct sockaddr *myaddr, int addrlen); // 성공 시 0, 실패 시 -1 리턴
~cpp 예제) if( bind(sockfd, (struct sockaddr *)&ina, sizeof(struct sockaddr) == -1 ) fprintf(stderr, "bind에서 에러가 났습니다.n")), exit(1);
2.2. listen - client의 요청을 기다린다! ¶
~cpp #include <sys/socket.h> int listen(int sockfd, int backlog); // 성공 시 0, 실패 시 -1 리턴 // backlog는 서버에 접속할 사람의 대기자 Maximum을 의미한다.
~cpp 예제) #deinfe BACKLOG 5 // 대기자가 5명이 넘으면 접속 불가능하다. if( listen(sockfd, BACKLOG) == -1 ) fprintf(stderr, "listen에서 에러가 났습니다.n"), exit(1);
2.3. accpet - client의 요청을 받아들인다! ¶
~cpp #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, int *addrlen); // 성공 시 파일 디스크립터, 실패 시 -1 리턴 // *addrlen에 주의. accept는 client의 인터넷 정보가 들어오면 addrlen의 크기(struct sockaddr_in의 크기)와 // 비교를 하여 크다면 받아들이지 않고, 작다면 크기를 줄일것이다.
// child process를 생성해 다중 연결을 하는 것은 설명하지 않습니다.
~cpp 예제 ) // int server_sock, client_sock // struct sockaddr_in server_addr, client_sock int sizeof_sockaddr_in; sizeof_sockaddr_in = sizeof(struct sockaddr_in); client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &sizeof_sockaddr_in); if( client_sock == -1 ) fprintf(stderr, "accept에러. client가 서버에 접속 할 수 없습니다.");
3.1. connect - Server에 연결한다. ¶
※ connect와 server 함수중 어떠한 함수가 닮았는지 이야기 해보자.
※ 이를 이야기 해보고 client의 프로그램의 네트워크 정보(struct sockaddr_in)에는 무엇이 들어가야하는지 이야기해보자.
※ 이를 이야기 해보고 client의 프로그램의 네트워크 정보(struct sockaddr_in)에는 무엇이 들어가야하는지 이야기해보자.
~cpp #include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); // 성공 시 0, 실패 시 -1 리턴
~cpp 예제 ) if( connect(client_sock, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) == -1 ) fprintf(stderr, "서버에 connect 할 수 없습니다."), exit(1);
4.1. close 파일을 닫는다. ¶
~cpp #include <unistd.h> int close(int fildes); // 성공 시 0, 실패 시 -1 리턴
~cpp 예제 ) close(sockfd);
5.1. send/write - 상대에게 데이터를 보낸다. ¶
~cpp #include <unistd.h> ssize_t send(int fildes, const void * buf, size_t nbytes, unsigned int flags); ssize_t write(int fildes, const void * buf, size_t nbytes); // 성공 시 전달 한 바이트 수, 실패 시 -1 리턴
~cpp 예제 ) char buf1[] = "Hello, World!"; char *buf2 = "Hello, World!"; if( send(client_sock, buf1, sizeof(buf), 0) == -1 ) fprintf(stderr, "send error"); if( write(client_sock, buf2, strlen(buf2)) == -1 ) fprintf(stderr, "write error"); // 타 시스템으로 이식을 위해 되도록 send를 사용하는 것이 좋다.
5.2. recv/read - 상대에게 데이터를 받는다. ¶
~cpp #include <unistd.h> ssize_t recv(int fildes, void *buf, size_t nbytes, unsigned int flags); ssize_t read(int fildes, void *buf, size_t nbytes); // 성공 시 수신 한 바이트 수(단 EOF만나면 0), 실패 시 -1 리턴
~cpp 예제 ) char buf1[200]; char buf2[200]; int str_len; str_len = recv(sockfd, buf1, sizeof(buf1), 0); str_len = read(sockfd, buf2, sizeof(buf2)); buf1[str_len] = 0; // 배열의 끝을 설정해준다. 하지 않으면 뒤의 쓰레기 값까지 접근된다. buf1[str_len] = 0; // 타 시스템으로 이식을 위해 되도록 send를 사용하는 것이 좋다.
6. ※ 윈도우 기반에서는... ¶
~cpp ◎ Visual C++에서 빈 소스 파일 하나를 연다. Project -> Setting -> LINK 메뉴 -> Object/library modules: 의 끝부분에 ws2_32.lib 를 추가한다. ◎ #include <sys/socket.h> -> #include <winsock2.h> ◎ UNIX 체계에서 사용하던 함수들의 헤더파일이 Windows 기반에서는 존재하지 않을 수도 있다. ◎ struct sockaddr_in -> SOCKADDR_IN ◎ int sockfd; -> SOCKET sockfd; // 소켓 디스크립터 // 소켓 핸들 ◎ main() 함수 내부에 WSADATA wsaDATA; // 추가. WSADATA형의 변수를 선언한다. if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0) fprintf(stderr, "WSAStartup Error"), exit(1); // 추가. WSAStartup() 은 socket의 버젼을 ws2_32 라이브러리에 전달한다. // 프로그램이 끝날 때, 항상 WSACleanup()으로 리소스를 해제해야한다.
~cpp 예제) #include <winsock2.h> main(){ WSADATA wsaDATA; SOCKET sockfd; // UNIX 기반의 int sockfd; SOCKADDR_IN ina; // UNIX 기반의 struct sockaddr_in ina; if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0) fprintf(stderr, "WSAStartup Error"), exit(1); ... 내용 WSACleanup(); }
7. server 예제 ¶
~cpp #include <stdio.h> #include <winsock2.h> #define PORT 9999 // 서버의 9999번 포트를 연다 #define BACKLOG 5 void error(char *buf) { puts(buf), exit(1); } main(){ WSADATA wsaData; SOCKET server_sock; // 서버의 socket을 생성 SOCKET client_sock; // 클라이언트의 socket을 생성 SOCKADDR_IN server_addr; // 네트워크의 정보를 담을 structure 생성. SOCKADDR_IN client_addr; int sizeof_sockaddr_in; char msg[] = "Hello, World!"; if( WSAStartup(MAKEWORD(2,2), &wsaData) == -1 ) error("WSAStartup Error"); // socket 설정 // 프로그래머는 이것을 통해 네트워크와 대화를 한다. server_sock = socket(AF_INET, SOCK_STREAM, 0); if( server_sock == -1 ) error("server socket error"); // 네트웍 정보 설정 // 이것은 프로그램이 socket과 연결할 정보를 담고있다. memset((SOCKADDR_IN *)&server_addr, 0, sizeof(SOCKADDR_IN)); // struct sockaddr_in -> SOCKADDR_IN server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; // 자신의 주소로 설정한다. server_addr.sin_port = htons(PORT); // socket과 네트웍 정보를 bind()로 연결한다. if( bind(server_sock, (sockaddr *)&server_addr, sizeof(SOCKADDR_IN)) == -1 ) error("bind() 에러"); // 준비는 끝났다! // listen()으로 client의 연결 요청을 기다리자. // client의 요청이 올 때 까지 서버는 여기서 기다린다. if( listen(server_sock, BACKLOG) == -1 ) error("listen() 에러"); // client의 요청이 오면 server는 accept() 함수로 요청을 받아들인다. // client와의 데이터 전송을 위해 client 소켓 디스크립터가 필요하다. sizeof_sockaddr_in = sizeof(SOCKADDR_IN); while(1) { client_sock = accept(server_sock, (sockaddr *)&client_addr, &sizeof_sockaddr_in); if( client_sock == -1 ) { // accept가 실패하면 while의 처음으로 돌아가 다시 client를 기다린다. fprintf(stderr, "accept() 에러"); continue; } send(client_sock, msg, sizeof(msg), 0); //close(client_sock); WSACleanup(); } exit(0); // exit로 종료를 하면 모든 파일 디스크립터를 자동으로 닫고 종료한다. }
※ 컴퓨터는 하나의 처리밖에 하지 못한다. 따라서 위의 소스는 하나의 client밖에 받아 들일 수 밖에 없다.
어떻게 하면 여러 client를 동시에 받아들일 수 있을까?
8. 실습 ¶
위의 server 에 접속 하는 client 프로그램을 짜고, Server가 보내는 메세지인 "Hello, World!"란 문장을 clinet 화면에 출력하도록 한다.
9. 참고 사이트 ¶
An Introduction to Socket Programming : http://www.uwo.ca/its/doc/courses/notes/socket/
Beej's Guide to Network Programming : http://www.ecst.csuchico.edu/~beej/guide/net/html/
Beej's 번역판 : 서치엔진에서 찾아보세요. (무책임)
Socket Programming in Python : http://www.amk.ca/python/howto/sockets/
IPv6 Socket Programming : http://www.joinc.co.kr/modules/moniwiki/wiki.php/article_IPv6_Programing?action=print2
----
프로그래밍분류
프로그래밍분류