소켓(Socket)이란 네트워크에서 데이터를 주고받는 두 장치 간의 종단점을 의미합니다.
컴퓨터 네트워킹에서 소켓은 통신 채널의 한 부분이며, 보통 인터넷 프로토콜(IP)와 함께 TCP/IP 네트워크에서 사용됩니다.
클라이언트 소켓과 서버 소켓은 네트워크 통신에서 중요한 역할을 함
이 두 종류의 소켓은 각각 다른 역할을 수행하며, 이들의 작동 방식은 클라이언트-서버 모델의 특성을 반영하고 있음
클라이언트 소켓: 클라이언트 소켓은 클라이언트 애플리케이션에서 생성되며, 서버에 연결을 요청하는 역할을 하고, 이 연결 요청은 일반적으로 서버의 IP 주소와 포트 번호를 명시하여 수행됨. 연결이 성공적으로 수립되면, 클라이언트 소켓은 서버 소켓과 데이터를 주고 받을 수 있게 된다.
서버 소켓: 서버 소켓은 서버 애플리케이션에서 생성되며, 클라이언트의 연결 요청을 기다리고 수락하는 역할을 하며, 서버 소켓은 특정 포트에서 들어오는 연결 요청을 감시(listen)하고, 요청이 들어오면 새로운 소켓을 생성하여 클라이언트와의 통신 채널을 열게 된다.

사진 출처 : https://www.it.uu.se/education/course/homepage/dsp/vt20/modules/module-2/sockets/
TCP 통신을 이용하여 데이터를 전송하기 위해 네트워크 연결을 설정(Connection Establish) 하는 과정
양쪽 모두 데이터가 전송할 준비가 되었다는 것을 보장하고, 실제로 데이터 전달이 시작하기전에 다른 한쪽이 준비되었다는 것을 알 수 있도록 함

#1 : 클라이언트는 서버에 접속을 요청하는 SYN(synchroniz) 패킷을 보냄, 이 때 클라이언트는 SYN을 보내고 SYN/ACK를 기다리는 SYN_SENT 상태가됨
#2 : 서버는 SYN요청을 받고 클라이언트에게 요청을 수락한다는 ACK 와 SYN flag가 설정된 패킷을 발송하고 클라이언트가 다시 ACK(acknowledgement)로 응답하길 기다림 (서버는 SYN_RECEIVED 상태가 됨)
#3: 클라이언트는 서버에 ACK를 보내고 이후로 데이터를 서로 주고 받게 됨 ( 서버는 EST ABLISHED 상태가 됨)
3 Way HandShake가 TCP를 초기화할 때 사용한다면, 4 Way HandShake는 세션을 종료하기 위해 수행되는 절차

#1 클라이언트가 연결을 종료하겠다는 FIN(Finish)플래그를 전송
#2 서버는 확인 메시지를 보내고 자신의 통신이 끝날때까지 기다리는데 이 상태를 TIME WAIT 상태라 함
#3 서버 통신이 끝났으면 연결이 종료되었다고 클라이언트에게 FIN플래그 전송
#4 클라이언트는 확인했다는 메시지를 보냄 (ACK)
서버 컴퓨터에서 동작하며 서비스를 제공하는 프로그램을 데몬 프로세스라고 한다.
데몬 프로세스는 크게 두 가지 형태가 있는데
반복 실행 서버는 서버 프로그램이 클라이언트의 요청을 직접 처리한다.
한 번에 한 클라이언트의 요청만 처리할 수 있고 여러 클라이언트가 서비스를 요청할 경우 순차적으로 처리되므로 클라이언트가 자기 순서를 기다려야 한다.
반복 실행 서버는 인터넷 소켓 TCP(SOCK_STREAM)를 이용해 통신한다.
#include <sys/socket.h> // 소켓 프로그래밍에 필요한 함수 & 상수 정의
#include <unistd.h>
#include <netinet/in.h> // 인터넷 프로토콜을 위한 구조체 & 상수 정의
#include <arpa/inet.h> // 인터넷 주소 변환 함수들을 정의
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9001 // 9001 포트 지정
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
memset((char *)&sin, '\0', sizeof(sin)); // 구조체 초기화
sin.sin_family = AF_INET; // 소켓 종류 AF_INET 지정 (ipv4)
sin.sin_port = htons(PORTNUM); // 포트 번호 지정
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // IP 주소 설정
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) { // ipv4 | TCP 사용
perror("socket");
exit(1);
}
if (bind(sd, (struct sockaddr *)&sin, sizeof(sin))) { // bind() 함수를 사용해 IP주소/포트 번호와 연결
perror("bind");
exit(1);
}
if (listen(sd, 5)) { // liten() 함수를 호출해 클라이언트의 요청을 받을 준비가 끝났음을 운영체제에 알림
perror("listen");
exit(1);
}
while (1) {
if ((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
} // accept() 함수를 사용해 클라이언트의 요청이 올 때까지 기다림
sprintf(buf, "%s", inet_ntoa(cli.sin_addr));
printf("*** Send a Message to Client(%s)\n", buf); // 어떤 클라이언트가 서버로 접속했는지 확인 & 출력
strcpy(buf, "Welcome to Network Server!!"); // 서버에서 클라이언트로 보낼 간단한 환영 메시지를 작성
if (send(ns, buf, strlen(buf) + 1, 0) == -1) { // send() 함수를 사용해 클라이언트로 보낼 메시지를 보냄
perror("send");
exit(1);
}
if (recv(ns, buf, sizeof(buf), 0) == -1) { // 클라이언트가 보낸 메시지를 recv() 함수로 받아서 출력
perror("recv");
exit(1);
}
printf("** From Client : %s\n", buf);
close(ns);
}
close(sd); // 작업이 끝나면 소켓을 닫음
}
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h> // 네크워크 데이터베이스 관련 구조체 & 함수 정의
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9001 // 클라이언트도 서버와 같은 포트 번호 사용
int main() {
int sd;
char buf[256];
struct sockaddr_in sin;
memset((char *)&sin, '\0', sizeof(sin)); // 구조체 초기화
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM); // 서버의 포트번호 지정
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // 접속할 서버의 주소를 지정
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // ipv4 | TCP 소켓을 생성
perror("socket");
exit(1);
}
if (connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
perror("connect");
exit(1);
} // 서버와 접속
if (recv(sd, buf, sizeof(buf), 0) == -1) { // 접속한 후 서버에서 보낸 메시지를 받아 출력
perror("recv");
exit(1);
}
printf("** From Server : %s\n", buf);
strcpy(buf, "I want a HTTP Service."); // 서버로 보낼 메시지를 복사한 후 메시지를 서버로 전송
if (send(sd, buf, sizeof(buf) + 1, 0) == -1) {
perror("send");
exit(1);
}
close(sd);
}
- 서버 프로그램 실행
- 서버는 socket() 함수를 사용하여 소켓을 생성합니다.
- bind() 함수를 사용하여 소켓에 IP 주소와 포트 번호를 바인딩합니다.
- listen() 함수를 호출하여 클라이언트의 연결 요청을 받을 준비를 합니다.
- accept() 함수를 사용하여 클라이언트의 연결 요청을 기다립니다.
- 클라이언트가 연결되면 새로운 소켓을 생성하여 클라이언트와의 통신을 처리합니다.
- 클라이언트 프로그램 실행
- 클라이언트는 socket() 함수를 사용하여 소켓을 생성
- connect() 함수를 사용하여 서버에 연결을 요청하고, 서버의 IP 주소와 포트 번호를 지정하여 연결
- 서버 클라이언트 통신
- 서버는 accept() 함수를 통해 클라이언트의 연결을 수락하면, 클라이언트로부터 메시지를 받을 준비를 함
- 클라이언트는 connect() 함수를 통해 서버에 연결되면, 서버로부터 메시지를 받을 준비를 함
- 서버는 send() 함수를 사용하여 클라이언트로 메시지를 전송
- 클라이언트는 recv() 함수를 사용하여 서버로부터 메시지를 수신
- 연결 종료
- 클라이언트나 서버에서 close() 함수를 사용하여 소켓을 닫아 연결을 종료
이 프로그램에서는 3-way handshake 및 4-way handshake 과정은 소켓 라이브러리 내부에서 자동으로 처리됨. socket() 함수로 소켓을 생성하고, connect() 함수로 서버에 연결을 요청하면 자동으로 3-way handshake가 수행되고, 연결 종료 시에는 close() 함수를 호출하여 소켓을 닫고, 이때 자동으로 4-way handshake가 수행된다.
서버 프로그램은 클라이언트의 서비스를 대신 처리할 프로세스를 fork() 함수로 생성하고 이 프로세스를 클라이언트와 연결한 다음 클라이언트의 접속을 기다린다.
#include <sys/socket.h> // 소켓 프로그래밍에 필요한 함수나 상수 정의
#include <unistd.h>
#include <netinet/in.h> // 인터넷 프로토콜을 위한 구조체 & 상수 정의
#include <arpa/inet.h> // 인터넷 주소 변환 함수들을 정의
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9002 // 포트 번호 지정
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
if (bind(sd, (struct sockaddr *)&sin, sizeof(sin))) { // bind() 함수를 사용하여 IP/PORT 연결
perror("bind");
exit(1);
}
if (listen(sd, 5)) { // 클라이언트 접속을 5개까지 처리하도록 설정
perror("listen");
exit(1);
}
while (1) {
if (( ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
} // accpet() 함수를 사용해 클라이언트의 접속 요청을 받음
switch (fork()) { // fork() 함수를 사용해 자식 프로세스를 생성하고, 자식 프로세스가 클라이언트의 응답을 처리하게 함
case 0:
strcpy(buf, "Welcome to Server");
if (send(ns, buf, strlen(buf) + 1, 0)) == -1) {
perror("send");
exit(1);
}
if (recv(ns, buf, sizeof(buf), 0) == -1) {
perror("recv");
exit(1);
}
printf("** From Client : %s\n", buf);
close(ns);
sleep(5);
exit(0);
}
}
close(sd);
}
이렇게 동시 동작하는 서버는 각 클라이언트와 독립적인 프로세스로 작동하며, 각각의 클라이언트와의 통신을 병렬로 처리할 수 있음. 이를 통해 여러 클라이언트의 동시 접속에 대응할 수 있는 서버를 구현이 가능하다.
이 프로그램을 실행하면 클라이언트가 접속했을 때 서버의 실행 상태에서 서버 프로그램이 하나 더 동작하고 있음을 알 수 있다.
서버 프로그램은 fork() 함수로 생성한 자식 프로세스가 클라이언트와 통신하게 된다. 이때 클라이언트에서 수행하는 부분이 부모 프로세스와 같은 파일에 작성되어 있어 exec() 함수를 사용하여 fork() 함수를 호출해 자식 프로세스를 생성한 후 exec() 함수로 다른 프로세스를 실행하게 할 것이다.
#include <sys/socket.h> // 소켓 프로그래밍에 필요한 함수나 상수 정의
#include <unistd.h>
#include <netinet/in.h> // 인터넷 프로토콜을 위한 구조체 & 상수 정의
#include <arpa/inet.h> // 인터넷 주소 변환 함수들을 정의
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9003 // 포트 번호 지정
int main() {
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // socket() 함수로 소켓을 생성
perror("socket");
exit(1);
}
perintf("** Create Socket\n");
memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // 포트번호와 IP 주소 설정
if (bind(sd, (struct sockaddr *)&sin, sizeof(sin))) { // 설정한 IP주소와 포트번호로 연결
perror("bind");
exit(1);
}
printf("** Bind Socket\n");
if (listen(sd, 5)) { // 최대 5개까지 접속을 처리하도록 지정
perror("listen");
exit(1);
}
printf("** Listen Socket\n");
while (1) {
if ((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) { // accept() 함수로 클라이언트의 요청을 받음
perror("accept");
exit(1);
}
printf("** Accept Client\n");
switch (fork()) { // fork() 함수로 자식 프로세스를 생성해 클라이언트의 응답을 처리
case 0:
printf("** Frok Client\n");
close(sd);
dup2(ns, STDIN_FILENO);
dup2(ns, STDOUT_FILENO); // 클라이언트와 통신할 수 있는 소켓 기술자(ns)를 표준 입력과 표준 출력으로 복사
close(ns);
execl("./han", "han", (char *)0); // execl() 함수로 han 프로세스를 실행
}
close(ns);
}
}
- dup2란?
- 파일 디스크립터를 복제하는 역할
- 디스크립터란?
- 파일과 관련된 작업을 수행하기 위해 사용되는 정수값
#include <unistd.h>
#include <stdio.h>
int main() {
printf("Welcome to Server, from Han!");
sleep(5);
}
#include <sys/socket.h> // 소켓 프로그래밍에 필요한 함수 & 상수 정의
#include <unistd.h>
#include <netdb.h> // 네크워크 데이터베이스 관련 구조체 & 함수 정의
#include <netinet/in.h> // 인터넷 프로토콜을 위한 구조체 & 상수 정의
#include <arpa/inet.h> // 인터넷 주소 변환 함수들을 정의
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9003 // 포트 번호 지정
int main() {
int sd, len;
char buf[256];
struct sockaddr_in sin;
memset((char *)&sin, '\0', sizeof(sin)); // 구조체 초기화
sin.sin_family = AF_INET; // 패밀리 이름을 AF_INET
sin.sin_port = htons(PORTNUM); // PORT 번호 설정
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // IP 주소 설정
if ((sd = socket(AF_INET, SOCKET_STREAM, 0)) == -1) { // ipv4 | TCP 소켓 생성
perror("socket");
exit(1);
}
printf("==> Create Socket\n");
if (connect(sd, (struct sockaddr *)&sin, sizeof(sin))) { //connect() 함수를 호출해 서버와 연결 요청
perror("bind");
exit(1);
}
printf("==> Connect Server\n");
if ((len = recv(sd, buf, sizeof(buf), 0)) == -1) { // 서버의 메시지를 받아 출력
perror("recv");
exit(1);
}
buf[len] = '\0';
printf("==> From Server : %s\n", buf);
close(sd); // 소켓을 닫음
}
이렇게 작성하면
==> Create Socket
==> Connect Server
==> From Server : Welcome to Server, from Han!
이런 메시지가 출력되는 것을 볼 수 있다.
소켓 기술자(Socket Descriptor)는 네트워크 프로그래밍에서 소켓과 관련된 작업을 수행하기 위해 사용되는 정수값이다.
서버 프로그램에서 클라이언트와 연결하는 소켓 기술자를 exec() 함수로 호출하는 프로세스에 명령행 인자로 전달하도록 지정한다. exec() 함수로 설정한 서비스 프로세스는 명령행 인자로 전달받은 소켓 기술자로 클라이언트와 통신한다.
#include <sys/socket.h> // 소켓 프로그래밍에 필요한 함수 & 상수 정의
#include <unistd.h> // 유닉스/리눅스 운영체제에 사용되는 다양한 시스템 호출 함수 정의
#include <netinet/in.h> // 인터넷 프로토콜 관련 함수 정의
#include <arpa/inet.h> // 인터넷 주소 함수 정의
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9004 // 포트번호 지정
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
printf("** Create Socket\n");
memset((char *)&sin, '\0', sizeof(sin)); // 구조체 초기화
sin.sin_family = AF_INET; // 패밀리 이름 = AF_INET
sin.sin_port = htons(PORTNUM); // 포트 번호 지정
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // IP 주소 지정
if (bind(sd, (struct sockaddr *)&sin, sizeof(sin))) { // IP주소:포트주소로 연결
perror("bind");
exit(1);
}
printf("** Bind Socket\n");
if (listen(sd, 5)) { // 최대 5개까지 접속 가능
perror("listen");
exit(1);
}
printf("** Listen Socket\n");
while (1) {
if ((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
} // acccpet() 함수를 사용해 클라이언트의 접속 요청을 받음
printf("** Accept Client\n");
switch (fork()) { // fork() 함수로 자식 프로세스를 생성하고 해당 자식 프로세스가 클라이언트의 응답을 처리하게 함
case 0:
printf("** Fork Client\n");
close(sd);
sprintf(buf, "%d", ns); // 클라이언트와 통신할 수 있는 소켓을 버퍼에 저장
execlp("./bit", "bit", buf, (char *)0 ); // execlp() 함수를 사용해 bit 프로세스를 호출할 때 buf에 저장한 소켓 정보를 인자로 함께 전송
close(ns);
}
close(ns);
}
}
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[]) {
char buf[256];
int len, ns;
ns = atoi(argv[1]); // 명령행 인자로 전달받은 소켓 기술자를 정수형으로 변환
strcpy(buf, "Welcome to Server, form Bit");
if ((send(ns, buf, strlen(buf) + 1, 0)) == -1) { // 간단한 환영 메지시를 클라이언트로 전송
perror("send");
exit(1);
}
if ((len=recv(ns, buf, sizeof(buf), 0)) == -1) { // 클라이언트에서 보낸 메시지를 받아서 출력
perror("recv");
exit(1);
}
printf("@@ [bit] From Client: %s\n", buf);
close(ns);
}
atoi() : c언어 문자열을 정수로 변환
#include <sys/socket.h>
#include <unistd.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define PORTNUM 9004
int main() {
int sd, len;
char buf[256];
struct sockaddr_in sin;
memset((char *)&sin, '\0', sizeof(sin)); // 구조체 초기화
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM); // 포트 넘버 지정
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // IP 주소 지정
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // ipv4 | TCP 소켓 생성
perror("socket");
exit(1);
}
printf("==> Create Socket\n");
if (connect(sd, (struct sockaddr *)&sin, sizeof(sin))) { // connect() 함수를 사용하여 연결
perror("connect");
exit(1);
}
printf("==> Connect Server\n");
if ((len = recv(sd, buf, sizeof(buf), 0)) == -1) { // 서버에서 메시지를 전달받음
perror("recv");
exit(1);
}
buf[len] = '\0';
printf("==> From Server : %s\n", buf);
strcpy(buf, "I want a SSH Service.");
if (send(sd, buf, sizeof(buf) + 1, 0) == -1) { // send() 함수를 통해 서버에 메시지 전송
perror("send");
exit(1);
}
close(sd); // 다쓴 소켓을 닫음
}
서버 프로그램
- 소켓을 생성 socket(AF_INET, SOCK_STREAM, 0)
- 소켓에 주소를 바인딩 bind(sd, (struct sockaddr *)&sin, sizeof(sin))
- 클라이언트의 연결 요청을 받기 위해 대기 listen(sd, 5)
- 클라이언트의 접속을 허용하고, 클라이언트와 통신할 소켓을 생성accept(sd, (struct sockaddr *)&cli, &clientlen)
- 자식 프로세스를 생성하여 클라이언트와의 통신을 담당 fork()
- 자식 프로세스에서는 클라이언트와 통신할 소켓 정보를 명령행 인자로 받아와 bit 프로그램을 실행 execlp("./bit", "bit", buf, (char *)0 )
- bit 프로그램
- 명령행 인자로 전달받은 소켓 기술자를 정수형으로 변환 ns = atoi(argv[1])
- 클라이언트로 환영 메시지를 전송 send(ns, buf, strlen(buf) + 1, 0)
- 클라이언트로부터 메시지를 수신하고 출력 recv(ns, buf, sizeof(buf), 0)
클라이언트 프로그램
- 서버에 연결하기 위해 소켓을 생성 socket(AF_INET, SOCK_STREAM, 0)
- 서버에 연결 connect(sd, (struct sockaddr *)&sin, sizeof(sin))
- 서버로부터의 메시지를 수신하고 출력 recv(sd, buf, sizeof(buf), 0)
- 서버에 메시지를 전송 send(sd, buf, sizeof(buf) + 1, 0)
이 프로그램에서도 3-way handshake 및 4-way handshake 과정은 소켓 라이브러리 내부에서 자동으로 처리됨.