E D R , A S I H C RSS

Unix Socket Programming And Windows Implementation

1. 기본적인 함수/개념들

1.1. Socket

※ 소켓이란?

~cpp 
#include <sys/types.h>
#include <sys/socket.h>

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

1.1.1. domain:

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

PF_INET6 IPv6 : 프로토콜 체계 사용

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

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

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

1.1.2. type: 서비스 타입

※ 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) 에러를 출력하고 프로그램 종료.
}

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 -> 인터넷 프로토콜 체계 사용.


~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으로 바꿔줘야한다.

1.2.3. sin_addr:

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");
*/

1.2.4. sin_zero''''''8'''''':

// 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. 정리에 필요한 함수


4.1. close 파일을 닫는다.

~cpp 
#include <unistd.h>

int close(int fildes);

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


~cpp 
예제 )

	close(sockfd);

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를 동시에 받아들일 수 있을까?

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

----
프로그래밍분류
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:28:20
Processing time 0.0665 sec