C# socket 통신

안녕하세요 wpf 초보자입니다.
외국 유튜브를 보면서 socket 통신을 이용한 채팅 프로그램을 만들어 봤는데
이게 영어로 입력하면은 출력이 잘 되지만 영어외에 다른 글자 한자, 한글등을 입력할 때 폰트가 깨지는 현상이 발생합니다.

<입력된 메시지를 바이트화?>
public string ReadMessage()
{
byte[] msgBuffer;
var length = ReadInt32();
msgBuffer = new byte[length];
_ns.Read(msgBuffer, 0, length);
var msg = Encoding.Default.GetString(msgBuffer);
return msg;
}

그래서 뭔가 바이트의 길이를 정하지 않아서 그런가 싶어서 [length]에 1024값을 줘봤는데 갑자기 깨짐 없이 잘 나옵니다.
1024값을 부여했을 때 왜 출력이 되었는지 아직까지 이해는 안됩니다.
문제가 해결 된 줄 알았지만, 콘솔창에는 사용자 이름과 채팅들이 문제 없이 잘 나오지만 메인 윈도우 창에서는 사용자이름만 나오고 보낸 메시지가 화면에 보이질 않습니다.

<바이트화 된 메시지를 콘솔, 윈도우 창에 보여지도록 하는 >
void Process()
{
while (true)
{
try
{
var opcode = _packetReader.ReadByte();
switch (opcode)
{
case 5:
var msg = _packetReader.ReadMessage();
Console.WriteLine($"[{DateTime.Now}]: 보낸 메시지 {Username} {msg}");
Program.BroadcastMessage($"[{DateTime.Now}]: 보낸 " + $“메시지 {Username} {msg}”);
break;
default:
break;
}
}

근데 웃긴건 Program.BroadcastMessage($"[{DateTime.Now}]: 보낸 " + $“메시지 {Username} {msg}”);
에서 {username}을 지우면 보낸 메시지가 출력이 된다는 것 입니다.

4일동안 이걸 해결못하고 계속 썼다가 고쳤다가 하고 있습니다… 도움 부탁드리겠습니다.
네이버 MYBOX 전체 소스 코드 zip파일로 압축해 놓은 곳 입니다.

좋아요 2

소스에서 Encoding.Default.GetBytes로 되어있는 부분을
Encoding.UTF8.GetBytes 로 전부 바꿔주시면 될겁니다. (서버,클라이언트 동일하게)
다만, 읽어들이는 Byte수를 정하는 부분에서 문제가 있어보입니다…
이것만 해결하시면 잘 될 것 같아요!

좋아요 1

UTF8, Default,ASCII 등등 다 바꿔봤는데 동일해요 ㅠㅠ
Byte수를 어떻게 정해주면 될까요?

좋아요 2

그래서 뭔가 바이트의 길이를 정하지 않아서 그런가 싶어서 [length]에 1024값을 줘봤는데 갑자기 깨짐 없이 잘 나옵니다.

기존에 length가 1이었던 것 같네요… buffer의 길이가 1글자만 읽을 수 있기에 그랬던 것 같습니다.

일반적으로 ascii는 1글자를 표현하는데 1byte가 필요합니다.
하지만 한글은 1글자를 표현하는데 2byte가 필요합니다.

인코딩 관련된 문제인 것 같은데, GUI쪽은 잘 모르겠어서 확실히 답을 못드리겠네요

좋아요 2

PacketReader의 ReadMessage()

var count = _ns.Read(msgBuffer,0,1024);
var msg = Encoding.Default.GetString(msgBuffer, 0, count);
return msg;

이런 식으로 바꿔 줘야 합니다.

그런데 Read() 메소드가 전체 패킷을 다 읽지 못하는 경우가 있으므로 BinaryReaderReadString()을 이용하는 것이 좋습니다. 보낼 때는 BinaryWriterWrite()로 메시지를 보내고요.

좋아요 3

1. 첫 번째 문제
PacketBuilder에서 메시지를 write할 때 메시지의 길이를 string.Length로 카운트하는 것에서 발생합니다.

string message의 길이와 인코딩 된 byte 배열의 길이가 다른데, 길이는 string.Length를 보내고 정작 메시지는 인코딩 된 byte 배열로 보내니 중간에서 메시지가 짤리게 되는 거죠. 반면에 바이트 배열의 길이를 1024로 고정해줬을 때는 그 범위 안에서는 문제가 없는 거구요.

2. 두 번째 문제
첫 번째 문제를 해결하기 위해 바이트 배열의 길이를 1024로 고정했기 때문에 발생합니다. UserName을 메시지와 동일한 프로토콜로 보내는데 바이트 배열이 1024로 고정되어 있으니 스트림에서 UserName을 읽는데만 1024 바이트를 다 쓰고 메시지는 짤려버리는 거죠.

@dimohy 님께서 제시해 주신 솔루션대로 길이를 맞춰주시면 될 것 같습니다.

PacketBuilder 클래스

public void WriteMessage(string msg)
{
    var encodedMessage = Encoding.Default.GetBytes(msg);

    var msgLength = encodedMessage.Length;
    _ms.Write(BitConverter.GetBytes(msgLength));
    _ms.Write(encodedMessage);
}

PacketReader 클래스

public string ReadMessage()
{
	var length = ReadInt32();
	byte[] msgBuffer = new byte[length];
	var count = _ns.Read(msgBuffer, 0, length);
	var msg = Encoding.Default.GetString(msgBuffer, 0, count);
	return msg;
}
좋아요 2

참고로 BinaryReader를 상속받아 PacketReader로 구현한 것은 지양하는 것이 좋습니다. 불필요하게 BinaryReader의 기능이 노출되니까요. 이것 대신 MessagePacket또는 ChatPacket으로 클래스를 만들고 내부적으로 BinaryReaderBinaryWriter를 쓰는 것이 깔끔합니다.

좋아요 3

답변 주셔서 정말 감사드립니다.

마지막으로 작성했던 내용이 send버튼과 보내지면서 사라지게 구연하려면 어떻게 하면 되는지 도움주시면 안될까요?
Button Click event가 발생할때 textbox의 " "값이 되도록 부여하였더니 전부다 없어지더라구요…
도움주셔서 정말 감사합니다.

좋아요 2

도움주셔서 너무 감사드립니다. 큰 도움이 되었습니다!

작성했던 내용이 send버튼과 보내지면서 사라지게 구연하려면 어떻게 하면 되는지 도움주시면 안될까요?
Button Click event가 발생할때 textbox의 " "값이 되도록 부여해보았는데 전체 내용이 사라지게 되더라구요…

좋아요 2

github 소스도 참고해보시면 도움 되실 것 같습니다. ^^

좋아요 2

요청한 현상을 추적하기 위해 조금 살펴봤는데, 일단 @루나시아 님의 글 대로 수정한 후 확인해보세요.

Connect할 때 서버에서 클라이언트로 연결된 사용자 정보를 보내는 부분을 처리하는 다음의 코드에서 오동작을 합니다.

image

좋아요 3

넵 해결 했습니다! 설명해주신 BinaryReader를 상속받아 PacketReader 사용하지 말고 MessagePacket 이나 ChatPacket같은 클래스를 만들어서 사용하라고 하신 부분에 대해서 제가 잘 이해가 안돼서요
저도 유튜브 보면서 하나 하나씩 처가면서 구현한거라서… 코드 하나 하나씩 어떻게 구현되고 동작되는지 이해하고 있는 중입니다…

좋아요 2

ViewModel에서 System.ComponentModel.INotifyPropertyChanged를 구현하고 속성이 변경되었음을 Notify 해야합니다.

class MainViewModel : INotifyPropertyChanged
{
	private string _message;
	public string Message
	{
		get => _message;
		set { _message = value; OnPropertyChanged(); }
	}

	public MainViewModel()
	{
		// 기타 생략
		SendMessageCommand = new RelayCommand(Send, o => !string.IsNullOrEmpty(Message));
	}

	public event PropertyChangedEventHandler? PropertyChanged;

	private void Send(object o)
	{
		if (string.IsNullOrEmpty(Message))
		{
			return;
		}
		_server.SendMessageToServer(Message);
		Message = string.Empty;
	}

	private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
	{
		PropertyChanged?.Invoke(this, new(propertyName));
	}
}

이렇게 Message의 Setter로 값이 변경될 때마다 OnPropertyChanged 메소드를 통해 PropertyChanged 이벤트를 발생시키면 해당 속성에 바인딩된 View에서 값 변경을 감지하고 변경값을 반영하게 됩니다.

참고로 ViewModel에서 INotifyPropertyChanged 구현이 안 되어 있는 경우 WPF의 바인딩 구조상 Memory leak을 유발하기 때문에 필수적으로 구현하셔야 합니다.
참고 링크: .NET Framework: 945. C# - 닷넷 응용 프로그램에서 메모리 누수가 발생할 수 있는 패턴

좋아요 3

제가 시간 될 때 유사한 채팅 프로그램을 만들어서 소스코드를 공유해 보도록 할께요 ^^;

좋아요 3

메시지를 두 번 보냈을 때부터 메시지를 정상적으로 감지하는 건, UserModel 생성 과정에서 ReadMessage를 두 번 실행하기 때문입니다. UID를 임의 부여하는 식으로 해보니 증상이 발생하지 않습니다. 공유해 주신 샘플에서는 해당 문제가 발생하지 않았던 것은 바이트 배열이 1024로 고정되어 있었기 때문에 UserName을 읽는데 1024 바이트를 쓰고, 메시지로 보낸 1024 바이트가 스트림에 남아서 이를 UID로 읽었기 때문일 것 같네요.

패킷으로부터 UserModel을 생성하는 과정을 잘 다듬어 보시면 될 것 같습니다.

좋아요 3

꼭 부탁드리겠습니다,
큰 도움 주셔서 감사합니다!

좋아요 3

루나시아님 과 dimohy님 덕분에 큰 도움 받아갑니다.
성심성의껏 질문에 응답해주시고 알려주셔서 감사합니다~

좋아요 3

참고 할 수 있도록 샘플 프로그램을 만들어봤습니다.

자잘한 예외 처리는 하지 않았으며, 채팅 기능만 집중하고 나머지 기능은 동작성만 확인할 수 있도록 간소화 하였습니다.,

| 최초 화면

| test 연결

| 디모이 연결

| 서버 화면

| 대화

패킷을 주고 받는 기능을 ChatPacket.cs에서 처리하였습니다.

| 패킷 받기

    public static async Task<IChatPacket> ReceiveAsync(Stream s)
    {
        try
        {
            var temp = new byte[1];
            await s.ReadExactlyAsync(temp, 0, 1);

            var command = (CommandType)temp[0];
            IChatPacket result = command switch
            {
                CommandType.REQ_HELLO => ChatHelloRequest.Receive(s),
                CommandType.RES_HELLO => ChatHelloResponse.Receive(s),
                CommandType.REQ_GOODBYE => ChatGoodbyeRequest.Receive(s),
                CommandType.RES_GOODBYE => ChatGoodbyeResponse.Receive(s),
                CommandType.REQ_MESSAGE => ChatMessageRequest.Receive(s),
                CommandType.RES_MESSAGE => ChatMessageResponse.Receive(s),
                CommandType.EVT_MESSAGE => ChatMessageEvent.Receive(s),
                CommandType.EVT_INFO => ChatInfoEvent.Receive(s),
                _ => new ChatPacketError(ChatPacketErrorKind.InvalidCommand)
            };

            return result;
        }
        // 강제 연결 종료
        catch (EndOfStreamException)
        {
            return new ChatPacketError(ChatPacketErrorKind.ForceDisconnected);
        }
        // 기타 알수 없는 이유 (패킷 비정상)
        catch (Exception)
        {
            return new ChatPacketError(ChatPacketErrorKind.InvalidPacket);
        }
    }

| 패킷 보내기

    public static void Send(IChatPacket packet, NetworkStream s)
    {
        s.WriteByte((byte)packet.Command);
        packet.Send(s);
        s.Flush();
    }

주거니 받거니 하는 패킷의 종류는 1 바이트의 Command로 구분하였으며 각각의 패킷은 IChatPacket 인터페이스를 구현합니다.

public interface IChatPacket
{
    CommandType Command { get; }
    void Send(Stream s);
}

public interface IChatPacket<T> : IChatPacket
{
    abstract static T Receive(Stream s);
}

다음은 메시지를 보내는 패킷의 코드입니다. (패킷 구성은 BinaryReader, BinaryWriter를 이용)

| ChatMessageRequest

public record ChatMessageRequest(string Authtoken, string Message) : IChatPacket<ChatMessageRequest>
{
    public CommandType Command => CommandType.REQ_MESSAGE;

    public static ChatMessageRequest Receive(Stream s)
    {
        using var br = new BinaryReader(s, Encoding.UTF8, true);
        return new ChatMessageRequest(br.ReadString(), br.ReadString());
    }

    public void Send(Stream s)
    {
        using var bw = new BinaryWriter(s, Encoding.UTF8, true);
        bw.Write(Authtoken);
        bw.Write(Message);
    }
}

패킷은 비동기 메소드인 ReceivedAsync()에 의해 수신되며 최초 연결시 등록됩니다.

_ = _client.ReceiveAsync();
_client.ReceivedChatPacketEvent += _client_ReceivedChatPacketEvent;

이후 이벤트를 통해 수신된 패킷을 처리합니다.

private void _client_ReceivedChatPacketEvent(object? sender, ReceivedChatPacketEventArgs e)
{
    var packet = e.Packet;

    if (packet is ChatPacketError error)
    {
        // 에러 처리 (생략함)
        return;
    }

    if (packet is ChatInfoEvent info)
    {
        if (info.InfoKind is ChatInfoKind.Users)
        {
            Users = info.InfoMap.Select(x => $"{x.Value}({x.Key})").ToList();
        }
    }
    else if (packet is ChatHelloResponse response)
    {
        // 인증키 처리 (생략함)
    }
    else if (packet is ChatMessageEvent message)
    {
        if (_lastDate != DateOnly.FromDateTime(DateTime.Now))
        {
            _lastDate = DateOnly.FromDateTime(DateTime.Now);
            Messages += $"[{_lastDate}]\r\n";
        }

        Messages += $"[{TimeOnly.FromDateTime(DateTime.Now)}, {message.Nickname}] {message.Message}\r\n";
    }
            
    _ = e.Client.ReceiveAsync();
}

참고가 되었으면 하네요.

좋아요 8

친절하게 알려주셔서 감사합니다.
비교해보면서 공부해보겠습니다!

좋아요 3