Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

UnixSocketProgrammingAndWindowsImplementation

From ZeroWiki

페이지의 컨텐츠를 보아하니, 따로 페이지를 뽑아내도 될것 같아 문서구조조정 하였습니다. 원래 페이지 이름은 데블스캠프2005/Socket Programming in Unix/Windows Implementation였습니다. - 임인택

주제 : Socket Programming의 기초적인 부분을 알아본다.

기본적인 함수/개념들

Socket

※ 소켓이란?
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 성공 시 파일 디스크립터, 실패 시 -1 리턴

domain:

PF_INET : 인터넷 프로토콜 체계 사용

PF_INET6 IPv6 : 프로토콜 체계 사용

PF_UNIX : 유닉스 방식의 프로토콜 체계 사용 (프로세스간 통신)

PF_NS XEROX : 네트워크 시스템의 프로토콜 체계 사용

PF는 Protocol Family PF대신 AF를 사용해도 무방. (ex. PF_INET -> AF_INET)


type: 서비스 타입

※ TCP/IP, UDP란?

SOCK_STREAM : 스트림 방식의 소켓 생성 (TCP/IP)

SOCK_DGRAM : 데이터그램 방식의 소켓 생성 (UDP)

SOCK_RAW : raw 모드의 소켓 생성


protocol: 프로토콜

IPPROTO_TCP : TCP 기반. 값은 0이다.

IPPROTO_UDP : UDP 기반. 값은 0이다. // 우리가 사용하는 프로토콜인 TCP, UDP가 0이므로 0으로 써도 무방하다. // 구체적인 프로토콜을 선택할 때 사용하는데 대부분의 응용 프로그렘에서는 0으로 지정하면 된다.


예제)

main(){
int sockfd;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);

	if(sockfd == -1)
		fprintf(stderr, "socket 함수에서 에러"), exit(1);
	// 에러가 났을경우( sockfd == -1) 에러를 출력하고 프로그램 종료.
}



네트워크 정보

struct sockaddr {

    u_short sa_family;    /* address family */

    char sa_data[14];    /* 주소(IP 주소 + 포트 번호) */

};
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가 같을까?


sin_family: // 주소체계

AF_INET : 인터넷 주소 체계

AF_UNIX : 유닉스 파일 주소 체계

AF_NS XEROX : 주소 체계

// sockaddr_in 은 TCP/IP체제 이므로 AF_INET만 사용한다. -> TCP/IP는 인터넷 기반이므로. // AF_INET/PF_INET -> 인터넷 프로토콜 체계 사용.

데이터를 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으로 변환 시켜주어야할까?


sin_port:

NULL : 임의의 포트를 할당한다. client에서 사용한다. // u_short sin_port 은 Big-Endian을 사용한다. // 따라서 Little_Endian을 사용하는 시스템에서는 Big-Endian으로 바꿔줘야한다.


sin_addr:

INADDR_ANY : 자기 자신의 주소를 할당한다. (== 0) // sin_addr은 인터넷 주소를 담고 있으므로 4 바이트가 필요하다. cf. 도메인 네임을 통한 연결은 설명하지 않겠습니다.

struct in_addr {
	unsigned long s_addr;
};
/*
	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'8':

// sin_zero 배열은 항상 0으로 채워져 있어야한다.

※ 왜 sin_zero가 만들어졌을까요?


예제)

#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;		// 서버의 경우
}


Server 가 될 프로그램에 필요한 함수

<img src="http://zeropage.org/pub/upload/sock.gif">

Bind - socket과 네트워크 정보를 연결하는 Bind!!!

#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *myaddr, int addrlen);
// 성공 시 0, 실패 시 -1 리턴
예제)

	if( bind(sockfd, (struct sockaddr *)&ina, sizeof(struct sockaddr) == -1 )
		fprintf(stderr, "bind에서 에러가 났습니다.n")), exit(1);


listen - client의 요청을 기다린다!

#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 성공 시 0, 실패 시 -1 리턴

// backlog는 서버에 접속할 사람의 대기자 Maximum을 의미한다.
예제)

#deinfe BACKLOG 5		// 대기자가 5명이 넘으면 접속 불가능하다.

	if( listen(sockfd, BACKLOG) == -1 )
		fprintf(stderr, "listen에서 에러가 났습니다.n"), exit(1);


accpet - client의 요청을 받아들인다!

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, int *addrlen);
// 성공 시 파일 디스크립터, 실패 시 -1 리턴
// *addrlen에 주의. accept는 client의 인터넷 정보가 들어오면 addrlen의 크기(struct sockaddr_in의 크기)와
// 비교를 하여 크다면 받아들이지 않고, 작다면 크기를 줄일것이다.

// child process를 생성해 다중 연결을 하는 것은 설명하지 않습니다.

예제 )

// 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가 서버에 접속 할 수 없습니다.");


Client 가 될 프로그램에 필요한 함수

<img src="http://zeropage.org/pub/upload/sock.gif">

connect - Server에 연결한다.

※ connect와 server 함수중 어떠한 함수가 닮았는지 이야기 해보자.
※ 이를 이야기 해보고 client의 프로그램의 네트워크 정보(struct sockaddr_in)에는 무엇이 들어가야하는지 이야기해보자.
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
// 성공 시 0, 실패 시 -1 리턴
예제 )

	if( connect(client_sock, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) == -1 )
		fprintf(stderr, "서버에 connect 할 수 없습니다."), exit(1);


정리에 필요한 함수

close 파일을 닫는다.

#include <unistd.h>

int close(int fildes);

// 성공 시 0, 실패 시 -1 리턴
예제 )

	close(sockfd);


server/client 공통 - 입출력 함수

<img src="http://zeropage.org/pub/upload/sock.gif">

send/write - 상대에게 데이터를 보낸다.

#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 리턴
예제 )

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를 사용하는 것이 좋다.


recv/read - 상대에게 데이터를 받는다.

#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 리턴
예제 )

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를 사용하는 것이 좋다.






※ 윈도우 기반에서는...

◎ 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()으로 리소스를 해제해야한다.
예제)

#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();

}




server 예제

#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 화면에 출력하도록 한다.


참고 사이트

An Introduction to Socket Programming : [1]

Beej's Guide to Network Programming : [2]

Beej's 번역판 : 서치엔진에서 찾아보세요. (무책임)

Socket Programming in Python : [3]

IPv6 Socket Programming : [4]


프로그래밍분류