네트워크 관련 공부 정리내용이며 참고서는 윤성우의 열혈 TCP/IP 소켓프로그래밍입니다.
실행 환경은 Ubuntu 20.04 Vscode & Windows10 Visual Studio2019 입니다.
소스코드 : https://github.com/hustle-dev/SocketProg
소켓 완성과정을 전화기에 비유
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 성공 시 파일 디스크립터, 실패 시 -1 반환
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 성공시 0 실패 시 -1 반환
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 성공시 0, 실패 시 -1 반환
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 성공시 파일 디스크립터, 실패 시 -1 반환
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char * meassage);
int main(int argc, char * argv[])
{
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello World!";
if(argc!= 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))== -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accep() error");
write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
}
void error_handling(char * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
서버 부분에 해당하는 코드이며 앞에서 설명한 socket, bind, listen, accept함수들이 들어가 있음을 확인할 수 있음 --> 서버 소켓
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
// 성공 시 0, 실패 시 -1 반환
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char * message);
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if(argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error!");
str_len = read(sock, message, sizeof(message) - 1);
if(str_len == -1)
error_handling("read() error!");
printf("Message from server: %s \n", message);
close(sock);
return 0;
}
void error_handling(char * message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
connect 함수를 통해 연결요청을 하고 있는 모습을 볼 수 있다. --> 클라이언트 소켓
리눅스는 소켓을 파일의 일종으로 구분하여 파일 입출력 함수를 소켓 입출력에, 즉 네트워크 상에서의 데이터 송수신에 사용할 수 있다. 반면 윈도우는 파일과 소켓을 구분하고 있어서 데이터 송수신 함수를 따로 참조해야된다.
저 수준: 표준에 상관없이 운영체제가 독립적으로 제공하는~ 의 의미
리눅스의 파일 디스크립터
0 : 표준입력 Standard Input
1 : 표준출력 Standard Output
2 : 표준에러 Standard Error
--> 자동으로 할당되는 파일 디스크립터들
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
// 성공 시 파일 디스크립터, 실패 시 -1 반환
#inclde <unistd.h>
int close(int fd);
// 성공 시 0, 실패시 -1 반환
#include <unistd.h>
ssize_t wrtie(int fd, const void * buf, size_t nbytes);
// 성공 시 전달한 바이트 수, 실패 시 -1 반환
size_t 와 ssize_t는 시스템에서 정의하는 자료형이기 때문에 _t가 붙고 일반적으로 typedef 선언을 통해 size_t는 unsigned int로 ssize_t는 signed int로 정의 되어 있다고 한다. 이러한 자료형을 사용하는 이유는 과거 16비트이던 시절부터 시스템의 차이나 시간의 흐름에 따라 자료형의 표현방식이 달라지기 때문에 선택된 자료형의 변경을 최소화 하기 위해 4바이트 자료형이 필요한곳에 이러한 자료형을 사용하였다고 한다.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
int main(void)
{
int fd1, fd2, fd3;
fd1 = socket(PF_INET, SOCK_STREAM, 0);
fd2 = open("test.dat", O_CREAT|O_WRONLY|O_TRUNC);
fd3 = socket(PF_INET, SOCK_DGRAM, 0);
printf("file descriptor 1: %d\n", fd1);
printf("file descriptor 2: %d\n", fd2);
printf("file descriptor 3: %d\n", fd3);
close(fd1); close(fd2); close(fd3);
return 0;
}
출력 결과는 fd1 = 3, fd2 = 4, fd3 = 5로 나오게 되는데 그 이유로 파일 디스크립터의 0, 1, 2는 표준 입출력에 이미 할당이 되었기 때문이다.
소켓 프로그래밍의 경우 윈도우와 리눅스 둘이 유사하기 때문에 동시에 공부하는 것이 효과적이라고 함.
윈속 프로그래밍을 하는 경우 반드시 WSAStartup함수를 호출해 라이브러리의 초기화 작업이 필요하다.
#include <winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 성공시 0, 실패 시 0이 아닌 에러코드 값 반환
#include <winsock2.h>
int WSACleanup(void);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
보통 프로그램 종료 직전 위의 함수를 호출
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
// 성공 시 소켓 핸들, 실패 시 INVALID_SOCKET 반환
int bind(SOCKET s, const struct sockaddr * name, int nameLen);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
int listen(SOCEKT s, int backlog);
// 성공시 0, 실패 시 SOCCKET_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 반환
int closesocket(SOCKET s);
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
서버 코드
#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("WSAStartUp() 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)
ErrorHandling("bind() error");
if (listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &szClntAddr);
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);
}
클라이언트 코드
#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);
}
#include <winsock2.h>
int send(SOCKET s, const char * buf, int len, int flags);
// 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환
int recv(SOCKET s, const char * buf, int len, int flags);
// 성공 시 수신한 바이트 수(단 EOF 전송시 0), 실패 시 SOCKET_ERROR 반환
send와 recv모두 리눅스에서 또한 사용이 됨. 책에서 read와 write를 당분간 사용하는 이유는 리눅스에서는 파일의 입출력과 소켓의 입출력이 동일함을 강조하기 위함이고 윈도우에서는 파일의 입출력과 소켓의 입출력이 다르기 때문에 다른 함수인 send와 recv를 써야만 함