: 네트워크를 통해 서버로부터 데이터를 가져오기 위한 통신 방식 2가지
: C언어를 공부하면서 처음 접하는 내용은 printf와 scanf를 이용하여 키보드와 모니터로 데이터를 주고 받는 정도였는데, 화면과 키보드(콘솔)를 통해 입출력하는 대상을 파일로 바꾸어서 파일 입출력을 공부해본 적이 있다. 콘솔 입출력과 파일 입출력에는 어떠한 공통점이 있다고 생각되지 않는가?
파일 입출력과 소켓 통신 사이에도 어떠한 공통점이 있는데, 파일은 로컬에서 통신, 소켓은 멀리 떨어진 호스트들간의 통신이다.
소프트웨어 차원에서 호스트들 간에 연결해주는 장치
소켓은 다소 추상적인데, 집에 있는 전화기를 예를 들어 생각해보면, 분명 멀리 떨어진 누군과와 대화하기 위해 필요한 물건이다. 이 전화기가 바로 소켓의 개념이다.
즉 멀리 떨어진 두 개체를 연결해주는 도구가 바로 소켓인 것이다.
예를 들어 전화를 걸고 싶은데 무엇이 필요할까?
단연코 전화기가 필요하고, 전화기를 멀리 떨어져 있는 두 사람이 서로 대화할 수 있도록 해야한다. 소켓은 멀리 떨어져 있는 호스트를 연결시켜주는 매개체 역할을 한다.
// 소켓 구현하기
int socket(int domain, int type, int protocol);
전화기를 얻고 나면, 통신을 위한 전화번호가 필요하다. 우리가 구입한 전화기에 전화번호를 할당한다는 의미로, 소켓도 전화번호에 해당하는 IP 주소를 할당해야한다.
// IP주소와 Port 할당하기
int bind(int sockfd, struct sockaddr *myaddr, int addrlen);
int bind(SOCKET s, const struct sockaddr FAR *name, int namelen);
전화번호까지 할당받았다면 이제 물리적으로 케이블을 연결해야 전화를 받을 수 있는 상태가 된다. 소켓도 연결 요청이 가능한 상태가 되어야 한다.
// 연결 요청 대기 상태로 진입
int listen(int sockfd, intf backlog);
int listen(SOCKET s, int backlog);
마지막으로 전화벨이 울리면 수화기를 들어서 전화를 받는다. 이 때, 수화기를 들었다는 것은 누군가 연결 요청을 했는데 이를 수락하겠다는 의미이다. 소켓도 누군가 연결을 요청해오면 그 요청을 수락할 수 있어야 한다
// 연결 요청 수락
int accept(int sockfd, struct sockaddr *addr, int addrlen);
SOCKET accept(SOCKET sockfd, struct sockadr FAR *addrlen);
// 연결 요청
int connect(SOCKET s, const struct sockaddr FAR *name, int namelen);
여기서 accept에서 성공을 하면 새로운 소켓을 생성한다. 이 때 왜 새로운 소켓을 또 생성하는 것일까??
기존의 소켓은 계속 들어오는 클라이언트의 요청을 받아야하고, 받은 요청을 또 어디론가 보내서 요청을 처리해야하기 때문에, 이 요청을 통신할 새로운 소켓이 필요한 것이다.
데이터를 호스트에 전송 및 수신
// 데이터 전송
int send(SOCKET s, const char FAR *buf, int len, int flag);
// 데이터 수신
int recv(SOCKET s, char FAR *buf, int len, int flag);
컴퓨터 상호간에 대화에 필요한 통신 규약
두 호스트간에 데이터를 주고 받을 수 있도록 프로그램을 구현하는 것을 네트워크 프로그래밍이라고 정의한다. 그런데 데이터를 어떻게 주고받는 것일까? 현실 세계에서는 전화나 편지 등등을 통해서 데이터를 주고 받는다고 할 때, 두 사람이 데이터를 주고 받는 경우 한 사람은 전화를 또 다른 상대방은 편지를 사용할 수 없게된다.
기본적으로 두 사람이 데이터를 주고 받기 위해서는 데이터를 주고 받는 방법을 약속할 필요가 있다. 이것이 바로 프로토콜이다.

인터넷 상에 존재하는 호스트들을 구분하기 위한 32비트 체계
점이 찍힌 십진수 표현 방식을 사용해서 IP 주소를 표현하며, 점에 의해 구분되는 값은 각각 1바이트로 표현되고, 총 4바이트를 사용한다.
컴퓨터는 보통 물리적인 네트워크 카드를 통해서 하나의 IP 주소를 갖는데 어떻게 수신한 데이터를 구분하여 각각의 프로그램에 전달해줄 수 있는 것일까? IP 주소로는 인터넷에 연결된 호스트를 구분할 수는 있어도 컴퓨터 안에서 실행되는 프로그램까지 구분하여 주지는 못한다. 이 때 필요한 것이 포트이다.
포트는 2바이트로 표현되므로 가질 수 있는 값의 범위는 0~65545까지로, 0~1024까지는 잘 알려진 포트로 이미 예약된 포트이다.
IP와 포트의 차이에 대해 예를 들어 설명하자면, 친구가 우리집 주소(=IP)를 안다고 해도, 집 안에서 용건을 가지고 있는 '나'를 찾아야 하는데, 내가 어디 있는지 알고 있는 것이 바로 Port인 것이다.

TCP 기반의 서버 프로그램은 다음과 같은 순서로 구현된다.

대기 큐에 들어있는 연결 요청을 순서대로 수락해야하는데, 연결 요청을 수락한다는 것은 요청한 클라이언트와 데이터를 주고 받을 수 있는 상태가 되는 것을 의미한다. 이 때, 데이터를 주고 받으려면 소켓이 있어야 한다. 연결을 위한 서버 소켓을 미리 만들어놓았으니 이것을 사용하면 될 것이라고 생각할 수도 있다. 즉, 서버 소켓은 문지기인 것이다. 클라이언트와 데이터를 주고 받기 위해서 문지기를 부르면 문은 누가 지키겠는가? 그래서 기존의 소켓은 계속 들어오는 클라이언트의 연결 요청을 받아서 처리하고 클라이언트와 데이터를 주고 받기 위해서는 새로운 소켓을 하나 더 만드는 것이다.
int accept(int s, struct sockaddr *addr, int *addrlent);
- s: 서버 소켓의 파일 디스크립터를 인자로 전달
- addr: 연결 요청을 수락할 클라이언트의 주소 정보를 저장할 변수의 포인터
- addrlen: 함수 호출이 성공적으로 끝나게되면, 클라이언트의 주소 정보길이가 바이트 단위로 채워짐
accept 함수는 "연결요청 대기 큐"에서 대기중에 잇는 클라이언트의 연결 요청을 수락해주는 함수로, 호출 성공 시, 내부적으로 클라이언트와의 데이터 입출력을 위해 사용할 소켓을 생성하고 그 소켓의 파일 디스크립터를 리턴해준다. 중요한 점은 소켓을 알아서 생성해준다는 점이다.

서버 구현 방법보다 간단한데, 소켓을 생성하고 연결하고자 하는 서버의 주소 정보를 가지고 바로 연결 요청을 하면 된다. 연결 요청 과정은 클라이언트 소켓을 생성한 후 서버에 연결을 요청하는 과정이다.

연결 요청 상태는 conncet 함수가 호출되는 시점의 상태이다.
int connect(int sockfd,struct sockaddr *rcv_addr, int *addrlen);
- sockfd: 미리 생성해놓은 소켓의 파일 디스크립터로, 클라이언트도 연결을 요청하고 데이터를 송수신하기 위해서는 기본적인 소켓이 있어야한다.
- serv_addr: 연결 요청을 보낼 서버의 주소 정보를 지닌 구조체 변수의 포인터
- addrlen: serv_addr 포인터가 기리키는 주소 정보 구조체 변수의 크기
connect 함수가 리턴되는 시점은

클라이언트가 전송해주는 데이터를 그대로 되돌려 전송해주는 기능의 서버

System.Net 네임 스페이스
System.Net 네임스페이스는 현재 네트워크에서 사용되는 여러 프로토콜에 대한 단순한 프로그래밍 이터페이스를 제공한다.
| 이름 | 설명 |
|---|---|
| DNS | 단순 도메인 이름 확인 기능을 제공 |
| IPAddress | IP(인터넷 프로토콜) 주소를 제공 |
| IPHostEntry | 인터넷 호스트 주소 정보에 대한 컨테이너 클래스를 제공 |
System.Net.Sockets 네임 스페이스
System.Net.Sockets 네임스페이스는 Window Socket 관련 인터페이스를 제공하며, 하위의 TcpClient, TcpListener 함께 제공
| 클래스 | 설명 |
|---|---|
| NetworkStream | 네트워크 액세스를 위한 내부 데이터 스트림을 제공 |
| TcpClient | TCP 네트워크 서비스에 대한 클라이언트 연결 제공 |
| TcpListener | TCP 네트워크 클라이언트에서 연결을 수신 |
TcpListener와 TcpClient 클래스
TcpListener 클래스는 server 역할을, TcpClient 클래스는 Client 역할을 한다.
| 클래스 | 메소드 | 설명 |
| TcpListener | Start() | 연결 요청을 기다리기 시작한다. |
| AcceptTcpClient() | 연결 요청을 수락한다. 이 메쏘드는 TcpClient에 반환. | |
| Stop() | 연결 종료. | |
| TcpClient | Connect() | TCP 서비스를 제공하는 서버에 연결 요청. |
| GetStream() | 데이터를 주고받는데 사용하는 매개체인 NetworkStream을 가져옴. | |
| Close() | 연결 종료. |
// 네임스페이스 추가
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
using System.Windows.Forms.VisualStyles;
namespace 채팅앱_만들기
{
public partial class Form1 : Form
{
TcpListener Server; // 서버 소켓
TcpClient Client; // 클라이언트 소켓
NetworkStream Stream; // 네트워크 연결 스트림
StreamReader Reader;
StreamWriter Writer;
Thread receiveThread;
bool Connected;
private delegate void AddTextDelegate(string text);
public Form1()
{
InitializeComponent();
}
private void btnSend_Click(object sender, EventArgs e)
{
txtView.AppendText("Me: " + txtInput.Text + Environment.NewLine);
Writer.WriteLine(txtInput.Text); // 보내기
Writer.Flush();
txtInput.Clear();
}
private void Form1_Load(object sender, EventArgs e)
{
ThreadStart ts = new ThreadStart(Listen);
Thread thread = new Thread(ts);
thread.Start();
}
private void Listen()
{
AddTextDelegate addText = new AddTextDelegate(txtView.AppendText);
// 소켓 생성
IPAddress addr = new IPAddress(0);
int port = 클라이언트와 통신할 port 번호 입력(클라이언트와 동일);
Server = new TcpListener(addr, port); // 생성 및 바인딩
Server.Start(); // 서버 시작
Invoke(addText, "Server Start!" + Environment.NewLine);
Client = Server.AcceptTcpClient(); // 연결 수락 (클라이언트와 통신)
Connected = true;
Invoke(addText, "Connect To Client" + Environment.NewLine);
Stream = Client.GetStream();
Reader = new StreamReader(Stream);
Writer = new StreamWriter(Stream);
// 수신을 위한 스레드
ThreadStart ts = new ThreadStart(Receive);
Thread rcv_thread = new Thread(ts);
rcv_thread.Start();
}
private void Receive()
{
AddTextDelegate addText = new AddTextDelegate(txtView.AppendText);
while (Connected)
{
if (Stream.CanRead)
{
string tempStr = Reader.ReadLine();
if (tempStr.Length > 0)
{
Invoke(addText, "You: " + tempStr + Environment.NewLine);
}
}
}
}
}
}
namespace Tcp_클라이언트
{
public partial class Client : Form
{
TcpClient client; // 클라이언트 소켓
NetworkStream Stream; // 네트워크 연결 스트림
StreamReader Reader;
StreamWriter Writer;
Thread receiveThread;
bool Connected;
private delegate void AddTextDelegate(string text);
public Client()
{
InitializeComponent();
}
private void Client_Load(object sender, EventArgs e)
{
string IP = "통신을 주고받을 상대방 IP주소 입력";
int port = 서버와 통신할 port 번호 입력(서버와 동일);
client = new TcpClient();
client.Connect(IP, port);
Stream = client.GetStream();
Connected = true;
txtView.AppendText("Succeed Access to Server!" + Environment.NewLine);
Reader = new StreamReader(Stream);
Writer = new StreamWriter(Stream);
ThreadStart ts = new ThreadStart(Receive);
Thread thread = new Thread(ts);
thread.Start();
}
private void Receive()
{
AddTextDelegate addText = new AddTextDelegate(txtView.AppendText);
while (Connected)
{
if (Stream.CanRead)
{
string tempStr = Reader.ReadLine();
if (tempStr.Length > 0)
{
Invoke(addText, "You: " + tempStr + Environment.NewLine);
}
}
}
}
private void btnSend_Click(object sender, EventArgs e)
{
txtView.AppendText("Me: " + txtInput.Text + Environment.NewLine);
Writer.WriteLine(txtInput.Text); // 보내기
Writer.Flush();
txtInput.Clear();
}
}
}