Post

c# 공부(tcp통신) 2024.06.03

tcp 1:多 통신으로 채팅구현

youtube강의

동기 & 비동기

동기 (Synchronous)

개념: 동기 프로그래밍은 코드의 각 작업이 순차적으로 실행되는 방식. 한 작업이 완료될 때까지 다음 작업이 시작되지 않음. 개발자가 코드를 작성하고 실행 순서를 쉽게 이해할 수 있도록 함.

장점:

  1. 간단한 구현: 코드가 순차적으로 실행되므로 이해하기 쉽고 디버깅이 용이.
  2. 예측 가능성: 작업이 완료되는 순서를 예측하기 쉽다.

단점:

  1. 비효율성: 긴 작업이나 I/O 작업이 완료될 때까지 다른 작업을 진행할 수 없다.
  2. 응답성 저하: GUI 응용 프로그램에서는 인터페이스가 멈추거나 응답하지 않을 수 있다.

실사용 예:

  • 간단한 스크립트나 테스트 코드
  • 데이터 양이 적고 작업이 빠르게 완료될 때
1
2
3
4
5
6
public void FetchData()
{
    var data = GetDataFromServer(); // 서버로부터 데이터를 가져옴
    ProcessData(data); // 데이터를 처리함
}

비동기 (Asynchronous)

개념: 비동기 프로그래밍은 작업을 동시에 실행하여, 어떤 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 실행할 수 있게 함. 긴 작업이나 I/O 작업 중에 프로그램이 응답하지 않는 것을 방지할 수 있다.

장점:

  1. 효율성: I/O 작업이나 긴 작업을 수행하는 동안 다른 작업을 계속할 수 있다.
  2. 응답성 유지: 사용자 인터페이스가 멈추지 않고 응답성을 유지.

단점:

  1. 복잡성 증가: 코드가 복잡해지고 디버깅이 어려워질 수 있다.
  2. 자원 관리: 비동기 작업의 자원 관리가 복잡할 수 있다.

실사용 예:

  • 네트워크 요청, 파일 I/O 작업
  • 사용자 인터페이스가 있는 응용 프로그램
1
2
3
4
5
6
public async Task FetchDataAsync()
{
    var data = await GetDataFromServerAsync(); // 서버로부터 데이터를 비동기적으로 가져옴
    ProcessData(data); // 데이터를 처리함
}

직렬화 & 역직렬화

직렬화 (Serialization)

개념: 직렬화는 객체의 상태를 문자열, 바이트 배열, 또는 기타 포맷으로 변환하는 과정. 이를 통해 객체를 파일에 저장하거나 네트워크를 통해 전송할 수 있다. 직렬화는 객체의 필드 값을 보존하면서 변환할 수 있다.

장점:

  1. 저장 및 전송 용이: 객체의 상태를 저장하거나 전송하기 쉽게 변환할 수 있다.
  2. 다양한 포맷 지원: JSON, XML, 바이너리 등 다양한 포맷으로 직렬화할 수 있다.

단점:

  1. 성능 비용: 직렬화와 역직렬화 과정에서 성능 오버헤드가 발생할 수 있다.
  2. 보안 문제: 민감한 데이터를 직렬화할 때 보안에 주의.

실사용 예:

  • 객체를 파일로 저장할 때
  • 네트워크를 통해 객체를 전송할 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Text.Json;

public class Chathub
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public int RoomId { get; set; }
    public string Message { get; set; }

    public string ToJsonString()
    {
        return JsonSerializer.Serialize(this);
    }
}

var hub = new Chathub { UserId = 1, UserName = "User1", RoomId = 123, Message = "Hello" };
string json = hub.ToJsonString();
Console.WriteLine(json); // {"UserId":1,"UserName":"User1","RoomId":123,"Message":"Hello"}

역직렬화 (Deserialization)

개념: 역직렬화는 직렬화된 데이터를 다시 객체로 변환하는 과정. 이를 통해 저장된 데이터나 전송된 데이터를 원래의 객체 상태로 복원할 수 있다.

장점:

  1. 데이터 복원: 직렬화된 데이터를 원래의 객체 상태로 쉽게 복원할 수 있다.
  2. 포맷 독립성: 다양한 포맷의 데이터를 객체로 변환할 수 있다.

단점:

  1. 성능 비용: 역직렬화 과정에서도 성능 오버헤드가 발생할 수 있다.
  2. 보안 문제: 신뢰할 수 없는 데이터의 역직렬화는 보안 위험을 초래할 수 있다.

실사용 예:

  • 파일에서 데이터를 읽어 객체로 변환할 때
  • 네트워크를 통해 수신된 데이터를 객체로 변환할 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Text.Json;

public class Chathub
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public int RoomId { get; set; }
    public string Message { get; set; }

    public static Chathub? Parse(string json)
    {
        return JsonSerializer.Deserialize<Chathub>(json);
    }
}

string json = "{\"UserId\":1,\"UserName\":\"User1\",\"RoomId\":123,\"Message\":\"Hello\"}";
var hub = Chathub.Parse(json);
Console.WriteLine(hub.UserName); // User1

1강

서버만 시작하면 아무것도 할 수 없는 상태이지만 서로 통신은 가능한 동기 상태 (동기적으로 작동하다 보니 메인스레드가 블락킹 됨)

  • ChatServer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
     private void btnListen_Click(object sender, EventArgs e)
     {
       _listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 8000);
       // 스타트하게 되면 아래의 리스너는 위의 리스너의 ip와 포트로 실행하게 됨! 서버 실행.. 
       _listener.Start();
       
    
       TcpClient client = _listener.AcceptTcpClient();
       NetworkStream stream = client.GetStream();
       byte[] buffer = new byte[1024];
       int read = stream.Read(buffer, 0, buffer.Length);   
    
       string message = Encoding.UTF8.GetString(buffer, 0, read);
       MessageBox.Show(message);
       var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");
    
       stream.Write(messageBuffer);
    }
    
  • ChatClient
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    private async void btnConnect_Click(object sender, EventArgs e)
    {
      _client = new TcpClient();
      await _client.ConnectAsync(IPAddress.Parse("127.0.0.1"), 8000);
    }
    private async void btnSend_Click(object sender, EventArgs e)
    {
      NetworkStream stream = _client.GetStream();
    
      string text = "안녕하세요";
      var messageBuffer = Encoding.UTF8.GetBytes(text);
      stream.Write(messageBuffer, 0, messageBuffer.Length);
      byte[] buffer = new byte[1024];
      int read = await stream.ReadAsync(buffer, 0, buffer.Length);
    
      string message = Encoding.UTF8.GetString(buffer, 0, read);
      MessageBox.Show(message);
    }
    

2강

비동기적으로 수행가능하게 코드를 바꿈

  • server
1
2
3
4
5
6
private async void btnListen_Click(object sender, EventArgs e)
{
	TcpClient client = await _listener.AcceptTcpClientAsync();
	int read = await stream.ReadAsync(buffer, 0, buffer.Length);   

}

async와 읽을때 비동기로 ReadAsync바꿔줌

  • client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private async void btnConnect_Click(object sender, EventArgs e)
{
    _client = new TcpClient();
    await _client.ConnectAsync(IPAddress.Parse("127.0.0.1"), 8000);
}
private async void btnSend_Click(object sender, EventArgs e)
{
    NetworkStream stream = _client.GetStream();

    string text = "안녕하세요";
    var messageBuffer = Encoding.UTF8.GetBytes(text);
    stream.Write(messageBuffer, 0, messageBuffer.Length);
    byte[] buffer = new byte[1024];
    int read = await stream.ReadAsync(buffer, 0, buffer.Length);

    string message = Encoding.UTF8.GetString(buffer, 0, read);
    MessageBox.Show(message);
}

비동기적으로 수행되게 마찬가지고 async 사용

클라이언트가 접속할때까지 대기하니까 while문을 추가해서 무한루프를 걸어줌

  • server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private async void btnListen_Click(object sender, EventArgs e)
{
    _listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 8000);
    // 스타트하게 되면 아래의 리스너는 위의 리스너의 ip와 포트로 실행하게 됨! 서버 실행.. 
    _listener.Start();
  
    while (true)
    {
        TcpClient client = await _listener.AcceptTcpClientAsync();
        NetworkStream stream = client.GetStream();
        byte[] buffer = new byte[1024];
        int read = await stream.ReadAsync(buffer, 0, buffer.Length);

        string message = Encoding.UTF8.GetString(buffer, 0, read);
        MessageBox.Show(message);
        var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");

        stream.Write(messageBuffer);
    }
}

문제점은 첫 번째 클라이언트가 연결된 후 데이터를 읽는 동안, 다른 클라이언트의 연결을 처리하지 못함. while (true) 루프 내에서 첫 번째 클라이언트의 데이터 읽기가 완료될 때까지 두 번째 클라이언트의 연결 요청을 수락할 수 없기 때문임. 그래서 비동기적으로 따로 빼서 코드를 작성해줘야함

  • server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private async void btnListen_Click(object sender, EventArgs e)
{
    _listener = new TcpListener(IPAddress.Parse("127.0.0.1"), 8000);
    // 스타트하게 되면 아래의 리스너는 위의 리스너의 ip와 포트로 실행하게 됨! 서버 실행.. 
    _listener.Start();

    while (true)
    {
        TcpClient client = await _listener.AcceptTcpClientAsync();
        _ = HandleClient(client);
    }

}

private async Task HandleClient(TcpClient client)
{
    NetworkStream stream = client.GetStream();
    byte[] buffer = new byte[1024];
    int read = await stream.ReadAsync(buffer, 0, buffer.Length);

    string message = Encoding.UTF8.GetString(buffer, 0, read);
    MessageBox.Show(message);
    var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");

    stream.Write(messageBuffer);
}

새로운클라이언트 계속 접속이 가능하게 만들고 메세지 전송을 함수를 따로 빼줘서 언제든 동기적으로 전송할 수 있게 해줌

클라이언트가 메세지를 한 번 주게 되면은 전송을 해주고 끝이남 연결되는동안은 계속 메세지를 줄 수 있게 끔 해야해서 while문으로 또 바꿔주고 거기에 메세지를 집어넣어야함

그리고 read의 값이 0이라면 정상적인 data가 아니라고 판단해야해서 int read를 바깥으로 빼줘야함 while문에서는 !!

  • 기존 server코드
1
2
3
4
5
6
7
8
9
10
11
12
13
private async Task HandleClient(TcpClient client)
{
    NetworkStream stream = client.GetStream();

    byte[] buffer = new byte[1024];
    int read = await stream.ReadAsync(buffer, 0, buffer.Length);

    string message = Encoding.UTF8.GetString(buffer, 0, read);
    MessageBox.Show(message);
    var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");

    stream.Write(messageBuffer);
}
  • 업데이트 server코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    private async Task HandleClient(TcpClient client)
    {
      NetworkStream stream = client.GetStream();
      byte[] buffer = new byte[1024];
      int read;
      // AAAAAAAAAAAAAAAAAAAAA
      while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
      {
          string message = Encoding.UTF8.GetString(buffer, 0, read);
          listBox1.Items.Add(message);
          // MessageBox.Show(message);
          var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");
    
          stream.Write(messageBuffer);
      } 
    }
    

이렇게 돼서 int read가 0보다 큰 동안에만 while문이 실행되도록 하는거임

데이터가 만약에 들어오게 되면 버퍼를 스트림으로 변환하고 그 스트림을 서버가 클라이언트에게 보내줌

그러고 다시 대기상태로 바뀜

기존 그냥 메세지박스로 받는걸 form에서 listbox로 받을 수 있게 도구를 추가해주고 코드를 추가해줌

문제는 클라이언트에서 한 번에 계속 주면 서버에서 read가 쭉 쌓인(주석처럼) 상태로 간 다음 메세지가 표시가 됨

3강

2강의 문제점을 해결하려면 1번째의 방법으로는 개행 문자를 추가

  • server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private async Task HandleClient(TcpClient client)
{
    NetworkStream stream = client.GetStream();
    byte[] buffer = new byte[1024];
    int read;
    while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
    {
        
        string message = Encoding.UTF8.GetString(buffer, 0, read);
        var messages = message.Split("[END]").Where(s => !string.IsNullOrEmpty(s));
        foreach (string m in messages)
        {
            listBox1.Items.Add(m);
        }
        
        // MessageBox.Show(message);
        var messageBuffer = Encoding.UTF8.GetBytes($"Server : {message}");

        stream.Write(messageBuffer);
    }
    
}
  • client
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    private async void btnSend_Click(object sender, EventArgs e)
    {
      NetworkStream stream = _client.GetStream();
    
      string text = textBox1.Text + "[END]";
      var messageBuffer = Encoding.UTF8.GetBytes(text);
      for (int i = 0; i < 100; i ++)
      {
          stream.Write(messageBuffer, 0, messageBuffer.Length);
      }
    }
    

근데 이렇게 되면 문제는 가나다[END]를 유저가 입력하면 잘림 1024가 최대이기때문. 서버에 설정해놓은 크기보다 더 큰 데이터 값이 넘어오면 잘리는 문제가 생김 그걸 보완해주기 위해서 2번째방법을 씀

  • client
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    private async void btnSend_Click(object sender, EventArgs e)
    {
      NetworkStream stream = _client.GetStream();
    
      string text = textBox1.Text;
      var messageBuffer = Encoding.UTF8.GetBytes(text);
      var messageLengthBuffer = BitConverter.GetBytes(messageBuffer.Length);
      for (int i = 0; i < 100; i ++)
      {
          stream.Write(messageLengthBuffer, 0, messageLengthBuffer.Length);
          stream.Write(messageBuffer, 0, messageBuffer.Length);
      }
    }
    

클라이언트에서 먼저 메세지 크기를 서버에 넘기고 그다음에 메세지를 넘기는 방식으로 한다면 버퍼크기에 대한 제약이 사라짐 동적으로 버퍼크기를 생성하기 때문임

단점으로는 두번 전송하기 때문에 meg의 일기와 실질적인 msg 한 번 전송하는것보다는 서버에 트래픽이 생길 수 잇음 근데 데이터의 정확도도 중요하기 때문에 저 방법을 사용하는게 1번보다 나음

4강

공통적인걸 계속 만들면 귀찮기 때문에 4강에서는 클래스로 만들어서 관리

서버와 클라이언트의 소켓통신클래스를 만들기에 앞서 서버와 클라이언트 사이의 중개역할을 하는 허브 클래스를 만들어서 객체를 직렬화해서 필드를 쉽게 전달하고 데이터를 역직렬화해서 필드로 쉽게 받을 수 있게끔 작업을 할거임

일단 class 라이브러리 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace ChatLib.Models
{
    public class Chathub
    {
        // 역직렬화 작업은 아래와 같음
        public static Chathub? Parse(string json) => JsonSerializer.Deserialize<Chathub>(json);

        public int UserId { get; set; }
        public string UserName { get; set; } = string.Empty;

        public int RoomId { get; set; }
        public string Message { get; set; } = string.Empty;

        // 직렬화 작업은 아래와 같음 
        public string ToJsonString() => JsonSerializer.Serialize(this);
    }
}

이런식으로 만들고 server와 client폴더와 같은 위치에 둔다음.

server와 client에 솔루션 우클릭 후 추가 > 기존 프로젝트에서 .csproj 확장자를 추가해주고 server와 client의 종속성에 참조를 해주기!

근데 종속성이 안보이길래 .net 으로 시작하는 form 프로젝트는 안보임. 그래서 더 짧은 글의 form 프로젝트를 생성하고 다시 했음. 그리고 프로젝트 열때 폴더를 열어서 실행하지말고 sin을 열어야 실행이됨

This post is licensed under CC BY 4.0 by the author.