PKNU IoT 개발자 과정
TCP/IP Study Repository
- HW부분은 이미 국내 네트워크가 잘 활성화 되어 있으므로 신경 쓸 필요 없다.
- SW부분은 운영체제에서 소켓을 제공하니 이것을 바탕으로 프로그래밍 할 것.
- 서버 소켓의 흐름
socket(): 소켓 생성 -> bind(): 소켓에 IP와 포트 부여 -> listen(): 연결요청 가능한 소켓으로 설정 -> accept(): 연결요청에 대한 수락 - 클라이언트 소켓의 흐름
socket() -> connect(): 서버 소켓으로의 연결 요청
#include <sys/socket.h>
// 전화기 장만
int socket(int domain, int type, int protocol);
// 전화번호 부여
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 전화기의 케이블 연결
int listen(int sockfd, int backlog);
// 수화기를 든다
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#include <sys/socket.h>
// 전화기 장만
int socket(int domain, int type, int protocol);
// 전화를 건다
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
VMware + Ubuntu를 설치하여 환경 구성.
리눅스에서의 소켓 조작은 파일 조작과 동일하게 간주된다.
사용자가 소켓(or 파일) 생성 -> OS는 해당 소켓에 파일 디스크립터 부여
파일 디스크립터 : 파일 또는 소켓에 부여된 정수(0,1,2번은 표준에 할당되어있다.)
- 파일 열기
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//path: 파일 이름 문자열의 주소 값
int open(const char* path, int flag);
//flag: 파일의 오픈 모드 정보
//O_CREAT: 필요하면 파일을 생성
//O_TRUNC: 기존 데이터 전부 삭제
//O_APPEND: 기존 데이터 보존하고, 뒤에 이어서 저장
//O_RDONLY: 읽기 전용으로 파일 오픈
//O_WRONLY: 쓰기 전용으로 파일 오픈
//O_RDWR: 읽기, 쓰기 겸용으로 파일 오픈
- 파일 닫기
#include <unistd.h>
//fd: 소켓의 파일 디스크립터
int close(int fd);
- 파일에 데이터 쓰기
#incldue <unistd.h>
//signed int //buf: 전송할 데이터가 저장된 버퍼의 주소 값
ssize_t write(int fd, const void* buf, size_t nbytes);
//nbytes: 전송할 데이터의 바이트 수
해당 코드🎮
low_open.c
#include <unistd.h>
//buf: 읽을 데이터가 저장된 버퍼의 주소 값
ssize_t read(int fd, void* buf, size_t nbytes);
//nbytes: 전송할 데이터의 바이트 수
해당 코드🎮
low_read.c
해당 코드🎮
fd_seri.c
파일 디스크립터는 0,1,2 이후의 숫자로 순서대로 넘버링 되는 것을 볼 수 있다.
상당수의 서버가 리눅스 기반으로 개발되는 동시에
클라이언트 프로그램의 경우 윈도우 기반 개발이 대부분이다.
- 리눅스 기반 소켓 함수와 차이(함수 인수의 자료형)가 조금 있음을 알 것
- winsock2.h 헤더 파일을 포함 할 것
- ws2_32.lib 라이브러리를 링크시킬 것
- winsock의 초기화가 필요하다
winsock 프로그래밍시 반드시 WSAStartup()을 통해 프로그램 요구 윈도우 소켓의 버전을 알리고, 해당 버전 지원 라이브러리의 초기화를 진행해야 한다.
#include <winsock2.h>
//wVersionRequested: 윈도우소켓 버전 정보
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
//lpWSAData: WSADATA라는 구조체 변수의 주소 값
// wVersionRequested는 버전 정보를 WORD 타입으로 담고있는데, 1.2 버전의 경우 0x0201이 된다.
// 일일이 바이트 단위로 쪼개서 버전 정보를 작성하는 것이 번거로우니 다음 함수를 사용한다.
// MAKEWORD(1, 2); -> 주 버전 1, 부 버전 2, 0x0201 반환
// WSAStartup(MAKEWORD(1, 2), &wsaData)
winsock의 충분한 사용 뒤에 라이브러리를 해제하도록 한다.
int WSACleanup(void);
#include <winsock2.h>
SOCKET socket(int af, int type, int protocol);
int bind(SOCKET s, const struct sockaddr * name, int namelen);
int listen(SOCKET s, int backlog);
SOCKET accept(SOCKET s, struct sockaddr * addr, int * addrlen);
int connect(SOCKET s, const struct sockaddr * name, int namelen);
int closesocket(SOCKET s);
리눅스와 다르게 윈도우는 파일 입출력 함수와 소켓 입출력 함수가 구분되어있다.
int send(SOCKET s, const char * buf, int len, int flags);
//s: 데이터 전송 대상과의 연결을 의미하는 소켓의 핸들 값 전달.
//buf: 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달.
//len: 전송할 바이트 수 전달.
//flags: 데이터 전송 시 적용할 다양한 옵션 정보 전달.
int recv(SOCKET s, const char * buf, int len, int flags);
//s: 데이터 수신 대상과의 연결을 의미하는 소켓의 핸들 값 전달.
//buf: 수신된 데이터를 저장할 버퍼의 주소 값 전달.
//len: 수신할 수 있는 최대 바이트 수 전달.
//flags: 데이터 수신 시 적용할 다양한 옵션 정보 전달.
해당 코드🎮
server_win.c client_win.c
해당 코드🎮
server.c client_win.c
리눅스 서버는 우분투 OS에서, 윈도우 클라이언트는 윈도우 OS cmd를 통해 실행하였다.
서로 데이터를 주고 받기(대화) 위해서 정의해 놓은 규약/약속
#include <sys/socket.h>
//1. 프로토콜의 체계 //3. 사용할 프로토콜
int socket(int domain, int type, int protocol);
//2. 소켓의 타입
PF_INET(IPv4), PF_INET6(IPv6), PF_LOCAL(로컬), PF_PACKET(Low Level 소켓), PF_IPX(IPX 노벨)
등이 있지만 보편적으로 사용되는 PF_INET을 기준으로 학습한다.
소켓의 데이터 전송방식이며 각 체계별로 두 가지 이상의 방식을 가진다.
- 연결지향형 소켓(SOCK_STREAM)
- 데이터 소멸x
- 전송 순서대로 데이터가 수신
- 데이터의 경계x -> 총 3번의 write()를 통해 100바이트 전송했지만 수신 시 read()를 통해 한 번에 수신함(반대의 경우도 가능).
- 버퍼 다 차면 전송x -> 이 타입은 자신 그리고 연결된 소켓의 상태를 파악하며 송수신한다.
- 소켓 대 소켓의 연결은 반드시 일대일이어야 한다.
- 비 연결지향형 소켓(SOCK_DGRAM)
- 데이터 수신 순서는 전송 순서와 상관x
- 데이터 소멸 가능성o
- 데이터의 경계o -> 용량이 제한되므로 여러번 전송 or 수신을 해야한다.
- 연결x
한 체계 안에 소켓 타입이 동일한 프로토콜이 둘 이상 존재하는 경우를 위해 필요한 정보.
소켓 생성 함수의 세번짜 인자로 들어간다.
TCP의 데이터 경계x 특성을 확인하고자 한다. -> write()와 reade()의 호출 횟수를 불일치 시킨다.
해당 코드🎮
server.c tcp_client.c
기존의 server + read()호출 횟수를 바꾼 client.c
컴퓨터간 통신을 위한 주소
IPv4 : A(0 ~ 127), B(128 ~ 191), C(192 ~ 223), D(224 ~ 239), E(240 ~ 255)
컴퓨터간 통신으로 받은 데이터를 최종 목적지인 응용프로그램에 주기 위한 주소
-> 컴퓨터는 NIC(Network Interface Card)로 IP를 통해 들어온 정보를 컴퓨터 내부로 전송하고,
-> 내부로 전송된 정보의 PORT번호를 바탕으로 응용프로그램의 PORT소켓에 전달한다.
-> UDP와 TCP별로 나누어져있다.
-> 포트 할당 시 Well-known PORT 번호는 피해서 할당하도록 한다.
주소표현을 위한 구조체가 정의되어있으며, 이를 통해 주소 정보를 전달하게 된다.
cpu가 데이터를 메모리에 저장하는 방식은 두 가지
- Big Endian : 낮은 번지 부터 높은 자리 숫자 들어감
- Little Endian : 낮은 번지 부터 낮은 자리 숫자 들어감
Big Endian 시스템에서 0x1234 전송 -> Little Endian 시스템에서는 0x1234를 받지만 0x3412로 해석한다.
-> Big Endian방식으로 네트워크 바이트 순서가 통일됨.
-> Little Endian 시스템과 통신을 위해선 한 번의 Big Endian 정렬이 필요하게 된다.
해당 코드🎮
endian_conv.c
실행 컴퓨터가 Little Endian으로 정렬하는 cpu를 가지고 있으므로 위와 같이 나온다.
만약 Big Endian cpu로 처리했다면 전후변화가 없는 결과가 출력될 것이다.
inet_addr()사용한 변환
해당 코드🎮
inet_addr.c
addr2="1.2.3.256"은 주소 규칙을 넘어가므로 변환이 안됨을 볼 수 있다.
inet_addr() + 구조체 변수에 주소 저장
해당 코드🎮
inet_aton.c
inet_aton()의 반대 기능
해당 코드🎮
inet_ntoa.c
addr.sin_addr.s_addr=htonl(INADDR_ANY);
응용 - TCP(or UDP) - IP - LINK
socket - bind - listen - accept - read/write - close
해당 코드🎮
server.c
socket - connect - read/write - close
해당 코드🎮
client.c