페이지의 컨텐츠를 보아하니, 따로 페이지를 뽑아내도 될것 같아
문서구조조정 하였습니다. 원래 페이지 이름은
데블스캠프2005/Socket Programming in Unix/Windows Implementation였습니다. -
임인택
주제 : Socket Programming의 기초적인 부분을 알아본다.
※ 소켓이란?
~cpp
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 성공 시 파일 디스크립터, 실패 시 -1 리턴
PF_INET : 인터넷 프로토콜 체계 사용
PF_
INET6 IPv6 : 프로토콜 체계 사용
PF_UNIX : 유닉스 방식의 프로토콜 체계 사용 (프로세스간 통신)
PF_NS XEROX : 네트워크 시스템의 프로토콜 체계 사용
PF는 Protocol Family
PF대신 AF를 사용해도 무방. (ex. PF_INET -> AF_INET)
※ TCP/IP, UDP란?
SOCK_STREAM : 스트림 방식의 소켓 생성 (TCP/IP)
SOCK_DGRAM : 데이터그램 방식의 소켓 생성 (UDP)
SOCK_RAW : raw 모드의 소켓 생성
1.1.3. protocol: 프로토콜 ¶
IPPROTO_TCP : TCP 기반. 값은 0이다.
IPPROTO_UDP : UDP 기반. 값은 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) 에러를 출력하고 프로그램 종료.
}
~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 -> 인터넷 프로토콜 체계 사용.
~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으로 변환 시켜주어야할까?
NULL : 임의의 포트를 할당한다. client에서 사용한다.
// u_short sin_port 은 Big-Endian을 사용한다.
// 따라서 Little_Endian을 사용하는 시스템에서는 Big-Endian으로 바꿔줘야한다.
INADDR_ANY : 자기 자신의 주소를 할당한다. (== 0)
// 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");
*/
// sin_zero 배열은 항상 0으로 채워져 있어야한다.
※ 왜 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. Server 가 될 프로그램에 필요한 함수 ¶
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. Client 가 될 프로그램에 필요한 함수 ¶
3.1. connect - Server에 연결한다. ¶
※ connect와 server 함수중 어떠한 함수가 닮았는지 이야기 해보자.
※ 이를 이야기 해보고 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 리턴
5. server/client 공통 - 입출력 함수 ¶
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를 동시에 받아들일 수 있을까?
위의 server 에 접속 하는 client 프로그램을 짜고, Server가 보내는 메세지인 "Hello, World!"란 문장을 clinet 화면에 출력하도록 한다.