TCP/IP 소켓 프로그래밍 복습 1
참고문헌 - 윤성우 <윤성우의 열혈 TCP/IP 소켓 프로그래밍>
♨ 개인적 해석이 들어간 글임으로, 인지하지 못한 오류가 있을 수 있습니다 ♨
▣ 네트워크 프로그래밍과 소켓에 대한 간단한 이해
- 네트워크 프로그래밍 : 네트워크로 연결되어 있는 서로 다른 컴퓨터가 데이터를 주고 받는 것
-> 조건 : 물리적인 연결(인터넷) + 소프트웨어적 데이터 송수신(운영체제에서 제공하는 socket)
* Socket? : 물리적으로 연결된 네트워크상에서의 데이터 송수신에 사용할 수 있는 소프트웨어적인 장치
▣ 윈도우 기반으로 소켓 구현
- 필요사항
1. #include <WinSock2.h>
2. 추가 종속성에 'ws2_32.lib' 추가 (프로젝트 설정 -> 구성 속성 -> 링커 -> 입력 -> 추가 종속성)
3. winsock의 초기화
윈속 프로그래밍을 할 때에는 반드시 WSAStartup 함수를 호출해서, 프로그램에서 요구하는 윈도우 소켓의 버전을 알리고
해당 버전을 지원하는 라이브러리의 초기화 작업을 진행해야 한다
-> #include <WinSock2.h>
int WSAStartup(WORD wVersonRequested, LPWSADATA lpwSAData);
① ②
=> 성공 시 0, 실패 시 0이 아닌 에러코드 값
① wVersonRequested : 프로그래머가 사용할 윈속의 버전정보 전달
② lpwSAData : WSADATA라는 구조체 변수의 주소 값 전달
※ ① : 윈도우 소켓에는 버전이 존재한다
-> 사용할 소켓 버전정보를 WORD형(unsigned short, typedef로 정의된 것)으로 구성하여 ①로 보내야 한다
예) 소켓의 버전이 1.2라면, 1이 주 버전, 2가 부 버전 -> 0x0201을 ①로 전달
상위 8비트에 부 버전 정보 / 하위 8비트에 주 버전 정보를 표시
이때 MAKEWORD 매크로 함수를 이용하면 간단히 WORD형 버전정보 구성 가능
※ ② : WSADATA 구조체 변수의 주소 값 (LPWSADATA는 WSADATA의 포인터 형)
WSAStartup함수 호출이 완료된 후 해당 변수에는 초기화된 라이브러리의 정보가 채워진다
특별한 의미를 지니는 것이 아니지만, WSAStarup 함수를 호출하기 위해서는 반드시 WSADATA 구조체 변수의 주소 값을 전달해야 한다
- winsock 라이브러리의 해제
윈속관련 함수의 호출이 불가능해진다
더 이상 윈속관련 함수의 호출이 불필요할 때 호출하는게 국롤
-> #include <WinSock2.h>
int WSACleanup(void);
=> 성공 시 0, 실패 시 SOCKET_ERROR 반환
▣ 윈도우 기반의 소켓관련 함수
- #include <WinSock2.h>하고 시작
- 소켓 생성
SOCKET socket(int af, int type, int protocol);-> 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환
- 소켓 IP주소 및 PORT번호 할당
int bind(SOCKET s, const struct sockaddr* name, int namelen);-> 성공 시 소켓 핸들, 실패 시 SOCKET_ERROR 반환
- 소켓 연결요청 가능상태로 변경
int listen(SOCKET s, const struct sockaddr* name, int namelen);-> 성공 시 0, 실패 시 SOCKET_ERROR 반환
- 클라이언트의 연결요청에 대한 수락
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);-> 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환
- 클라이언트 프로그램에서 소켓을 기반으로 연결요청
int connect(SOCKET s, const struct sockaddr* name, int namelen);-> 성공 시 0, 실패 시 SOCKET_ERROR 반환
- 소켓 닫기
inr closesocket(SOCKET s);-> 성공 시 0, 실패 시 SOCKET_ERROR 반환
----------------------------------------------------------------------------------------------------------------------------------------------------------
예) hello_server_win.c (서버 소켓)
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* Message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAddr, clntAddr;
int szClntAddr;
char message[] = "Hello World!";
if (argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)// 소켓 라이브러리 초기화
ErrorHandling("WSAStartip() error!");
hServSock = socket(PF_INET, SOCK_STREAM, 0);// (서버)소켓 생성
if (hServSock == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if (bind(hServSock, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)// 서버소켓에 IP주소, PORT번호 할당(bind 함수)
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR)// 위에서 생성한 서버 소켓 완성(listen 함수)
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);// 클라이언트의 연결요청을 수락하기 위해 accept 함수 호출
if (hClntSock == INVALID_SOCKET)
ErrorHandling("accept() error");
send(hClntSock, message, sizeof(message), 0);
closesocket(hClntSock);
closesocket(hServSock);
WSACleanup();// 소켓 라이브러리 초기화
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
▣ 원도우에서의 파일 핸들과 소켓 핸들
- 리눅스는 내부적으로 소켓도 파일 취급한다 -> 파일/소켓을 생성하면 파일 디스크립터가 반환된다
- 윈도우에서 시스템 함수의 호출을 통해서 파일을 생성할 때 '핸들(handle)'이라는 것을 반환
-> 윈도우 : 핸들 = 리눅스 : 파일 디스크립터
but! 윈도우는 파일 핸들과 소켓 핸들을 구분하고 있다
-> 파일 핸들 기반의 함수와 소켓 핸들 기반의 함수에 차이가 있다
- 소켓관련 함수들의 SOCKET 자료형은 정수로 표현되는 소켓의 핸들 값을 저장하기 위해서 typedef 선언으로 정의된 것
▣ 윈도우 기반 서버, 클라이언트 예제의 작성
예) hello_client_win.c (클라이언트)
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
void ErrorHandling(char* Message);
int main(int argc, char* argv[])
{
WSADATA wsaData;
SOCKET hSocket;
SOCKADDR_IN servAddr;
char message[30];
int strLen;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)// 소켓 라이브러리 초기화
ErrorHandling("WSAStartup() error!");
hSocket = socket(PF_INET, SOCK_STREAM, 0);// 소켓 생성
if (hSocket == INVALID_SOCKET)
ErrorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr) == SOCKET_ERROR))// 서버에 여결 요청
ErrorHandling("connect() error!");
strLen = recv(hSocket, message, sizeof(message) - 1, 0);
if (strLen == -1)
ErrorHandling("read() error");
printf("Message from server : %s \n", message);
closesocket(hSocket);
WSACleanup();
return 0;
}
void ErrorHandling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
▣ 윈도우 기반 입출력 함수
int send(SOCKET s, const char* buf, int len, int flags);
//-> 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환
① s : 데이터 전송 대상과의 연결을 의미하는 소켓의 핸들 값 전달
② buf : 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달
③ len : 전송할 바이트 수 전달
④ flags : 데이터 전송 시 적용할 다양한 옵션 정보 전달
- ④을 제외하고 리눅스의 write 함수와 차이가 없다
-> ④은 일단은 설정하지 않는다는 의미로 0을 전달한다
int recv(SOCKET s, const char* buf, int len, int flags);
//-> 성공 시 수신한 바이트 수(단 EOF 전송 시 0), 실패 시 SOCKET_ERROR 반환
① s : 데이터 수신 대상과의 연결을 의미하는 소켓의 핸들 값 전달
② buf : 수신된 데이터를 저장할 버퍼의 주소 값 전달
③ len : 수신할 수 있는 최대 바이트 수 전달
④ flags : 데이터 수신 시 적용할 다양한 옵션 정보 전달