해당 시리즈의 모든 설명은 리눅스 기준임을 밝힙니다.
socket() 함수로 소켓을 생성하고 그에 해당하는 디스크립터(Descriptor)를 리턴받을 수 있다.(디스크립터에 대해서는 항목 2에서 설명하겠다.)
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 소켓 생성 성공시 해당 디스크립터, 실패시 -1 return.
이어 bind() 함수를 이용, 소켓에 주소정보를 할당해주어야 한다.
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
// 바인딩 성공시 0, 실패시 -1 return.
소켓을 만들었고, 주소정보 할당까지 완료하였으니 이제 통신 요청이 들어오기만을 기다리면 된다. 여기에는 listen() 함수를 사용한다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 성공시 0, 실패시 -1 return.
listen() 함수까지 호출하고 나면 비로소 통신 요청이 들어오기를 기다리기 시작한다. 통신 요청이 들어오면? accept() 함수로 연결을 승낙하면 된다!
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 성공시 통신에 필요한 디스크립터를, 실패시 -1을 return.
참고로 연결 요청이 없는 상태에서 accept() 함수를 호출하는 경우 새로운 연결 요청이 들어올 때까지 아무 것도 리턴하지 않고 기다리게 된다.
요약하면, 다음 순서로 통신이 이루어진다.
1. socket() // 소켓 생성
2. bind() // 주소 정보 할당
3. listen() // 연결 요청 대기
4. accept() // 연결 요청 승낙
5. 이후 write() or read() 사용하여 데이터 주고받기
소켓으로 서버를 만들 때보다 훨씬 더 간단하다. socket() 함수와 connect() 함수만 적절히 이용하면 되기 때문이다.
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
// 성공시 0, 실패시 -1 반환
클라이언트 소켓의 경우 다음 순서로 통신이 이루어지는 것이다.
1. socket() // 소켓 생성
2. connect() // 연결 요청
3. 이후 write() or read() 사용하여 데이터 주고 받기
기본 파일 디스크립터는 아래와 같다.
0 -> 표준 입력
1 -> 표준 출력
2 -> 표준 에러
아마 왜 뜬금없이 파일 얘기를 꺼내는지 다들 궁금할 것이다. 이유는 간단하다.
Q. 왜 뜬금없이 네트워크 얘기 하다가 파일 얘기 하세요?
A. 리눅스에서는 소켓도 파일과 같이 취급되기 때문입니다.
아무튼, 저수준 파일 입출력을 위해서 리눅스에서는 파일 디스크립터와 함께 open(), close(), write() 함수와 read() 함수를 제공한다. open() 함수 먼저 살펴보자.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);
// 성공시 파일 디스크립터 리턴, 실패시 -1 리턴
flag의 위치에는 아래의 상수들을 적절히 or 연산자(|)로 묶어 넣어주면 된다.
O_CREAT -> 필요시 파일 생성
O_TRUNC -> 파일 내용 전부 삭제
O_APPEND -> 기존 파일 내용 뒤에 이어서 저장
O_RDONLY -> 읽기 전용으로 열기
O_WRONLY -> 쓰기 전용으로 열기
O_RDWR -> 읽기/쓰기 겸용으로 열기
close() 함수는 정말 별 것 없다.
#include <unistd.h>
int close(int fd);
// 성공시 파일 0, 실패시 -1 리턴
다음은 write() 함수이다.
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
// 성공시 전달한 바이트 수, 실패시 -1
다음은 read() 함수이다.
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
// 성공시 받은 바이트 수, 실패시 -1(단, EOF 만나면 0)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(void){
int svr_sock;
int clnt_sock;
char msg[] = "Hell World!";
struct sockaddr_in svr_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_len;
svr_sock = socket(PF_INET, SOCK_STREAM, 0);
// 1. socket()
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(atoi(argv[1]));
bind(svr_sock, (struct sockaddr*)&svr_addr, sizeof(svr_addr));
// 2. bind()
listen(svr_sock, 5);
// 3. listen()
clnt_sock = accept(svr_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_len);
// 4. accept()
write(clnt_sock, msg, sizeof(msg));
// 데이터 전송
close(clnt_sock);
close(svr_sock);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[]){
int sock;
struct sockaddr_in svr_addr;
char msg[30];
int str_len;
sock = socket(PF_INET, SOCK_STREAM, 0);
// 1. socket()
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = inet_addr(argv[1]);
svr_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&svr_addr, sizeof(svr_addr));
// 2. connect()
str_len = read(sock, msg, sizeof(msg) - 1);
printf("Msg: %s\n", msg);
close(sock);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void){
int fd_rdr;
int fd_wtr;
char buf[1];
int i;
fd_rdr = open("1.jpg", O_RDONLY);
fd_wtr = open("1-1.jpg", O_CREAT | O_WRONLY | O_TRUNC);
if(fd_rdr == -1){
printf("open rdr error");
exit(1);
}
if(fd_wtr == -1){
printf("open wtr error");
exit(1);
}
while(read(fd_rdr, (void*)buf, sizeof(buf)) == 1){
write(fd_wtr, (void*)buf, sizeof(buf));
}
close(fd_rdr);
close(fd_wtr);
}
open() 함수를 이용해 읽기 전용 파일 디스크립터 하나, 쓰기 전용 파일 디스크립터 하나를 생성하고 read() 함수와 write() 함수를 이용해 데이터를 읽어오고-쓰고 있다. 마지막에는 close() 함수로 파일 디스크립터를 닫았다.