반응형

2021.12.23 - [C#] - C# TCP/IP Image transfer - 이미지(파일) 전송 1

2021.12.24 - [C#] - C# TCP/IP Image transfer - 이미지(파일) 전송 2

 

위 두 링크에서 만든 프로그램을 개선해 클라이언트의 화면을 서버로 연속 전송, 영상으로 재생해 보자.

간단한 화면공유(Screen Share) 프로그램을 만드는 것이다.

 

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Server
{
    public partial class Form1 : Form
    {
        TcpListener listener;
        TcpClient client;
        NetworkStream networkStream;
        MemoryStream memoryStream;
        Bitmap bitmap;
        IPHostEntry ipHostEntry;
        Thread thread;
 
        string serverIP;
        int serverPort;
        byte[] data;
        byte[] dataSizeFromClient;
        int receivedDataSize;
        int expectedDataSize;
 
        public Form1()
        {
            InitializeComponent();
 
            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
 
            serverPort = 7000;
            // 호스트 이름으로 검색되는 첫 번째 IP4 주소 확인
            string hostName = Dns.GetHostName();
            ipHostEntry = Dns.GetHostEntry(hostName);
            foreach (IPAddress address in ipHostEntry.AddressList)
            {
                if (address.AddressFamily == AddressFamily.InterNetwork)
                {
                    serverIP = address.ToString();
                    break;
                }
            }
            listBox1.Items.Add("Server IP: " + serverIP);
 
            data = new byte[1048576 * 10]; // 1MB * 10
                                           // 클라이언트로 부터 전송되는 PNG 파일은 상황에 따라 4MB를 넘기도 한다.
            dataSizeFromClient = new byte[sizeof(int)];
        }
 
        private void ThreadProc()
        {
            listener = new TcpListener(IPAddress.Any, serverPort);
            //listener = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
            listener.Start();
 
            client = listener.AcceptTcpClient();
            listBox1.Items.Add("Client IP: " + client.Client.RemoteEndPoint.ToString().Split(':')[0]);
            networkStream = client.GetStream();
 
            while (true)
            {
                try
                {
                    receivedDataSize = 0;
 
                    if (networkStream.CanRead)
                    {
                        // 클라이언트로 부터 받아야 할 데이터 사이즈 정보 확인.
                        networkStream.Read(dataSizeFromClient, 0, dataSizeFromClient.Length);
                        expectedDataSize = BitConverter.ToInt32(dataSizeFromClient, 0);
                        //listBox1.Items.Add("Expected data size: " + (expectedDataSize / 1024).ToString() + "KB");
 
                        // 데이터 수신.                        
                        do
                        {
                            // 클라이언트로 부터 받아야 할 데이터 사이즈 만큼만 정확히 받는다.
                            receivedDataSize += networkStream.Read(data, receivedDataSize, expectedDataSize - receivedDataSize);
                            // Reads data from the NetworkStream and stores it to a byte array.
                        } while (expectedDataSize != receivedDataSize);
                        //while (networkStream.DataAvailable);
                        // NetworkStream.DataAvailable 은 네트워크 상황에 따라 정확하지 않을 가능성이 크다.
                        
                        //listBox1.Items.Add("Data size: " + (receivedDataSize / 1024).ToString() + "KB");
                        // 클라이언트로 부터 받은 데이터 사이즈가 버퍼 사이즈(10MB) 보다 크다면 버퍼 사이즈를 늘려야 한다.
                        // 그렇지 않으면 NetworkStream.Read()에서 ArgumentOutOfRangeException 예외 발생.
                    }
 
                    //listBox1.Items.Add("Data received: " + (receivedDataSize / 1024).ToString() + "KB");
                    memoryStream = new MemoryStream(data, 0, receivedDataSize);
                    // Initializes a new non-resizable instance of the MemoryStream class
                    // based on the specified region (index) of a byte array.            
                    bitmap = new Bitmap(memoryStream);
                    pictureBox1.Image = bitmap;
                }
                catch (Exception e)
                {
                    listBox1.Items.Add(e.Message);
                    listBox1.Items.Add(e.StackTrace);
                    break;
                }
            }
 
            listener.Stop();
            client.Close();
            networkStream.Close();
            memoryStream.Close();
        }
 
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 클라이언트 접속 대기를 위한 스레드 생성(스레드는 1개만 생성한다)
            if (thread == null || !thread.IsAlive)
            {
                thread = new Thread(new ThreadStart(ThreadProc));
                thread.Start();
                listBox1.Items.Add("Waiting for a client...");
            }
        }
 
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (thread != null && thread.IsAlive)
            {
                // TcpListener를 정지 시키지 않고 클라이언트 연결 대기시 프로그램을
                // 종료하려 하면 스레드가 종료되지 않아 프로그램이 종료되지 않는다.
                listener.Stop();
 
                // 클라이언트와 연결된 스레드를 종료하지 않으면 프로그램을 종료해도
                // 백그라운드에서 계속 실행된다.                
                thread.Abort();
            }
        }
    }
}
 

 

서버 코드를 입력하고 빌드한다.

Server.zip
0.00MB

 

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
 
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.IO;
 
namespace Client
{
    public partial class Form1 : Form
    {
        TcpClient client;
        NetworkStream networkStream;
        MemoryStream memoryStream;
        Bitmap screen;
        Thread thread;
 
        string serverIP;
        int serverPort;
        byte[] data;
        byte[] dataSizeForServer;
 
        public Form1()
        {
            InitializeComponent();
 
            pictureBox1.SizeMode = PictureBoxSizeMode.StretchImage;
            textBox1.Text = "192.168.0.100";
        }
 
        private void ThreadProc()
        {
            serverIP = textBox1.Text;
            serverPort = 7000;
            try
            {
                client = new TcpClient(serverIP, serverPort);
                networkStream = client.GetStream();
                listBox1.Items.Add("Connected to: " + client.Client.RemoteEndPoint.ToString().Split(':')[0]);
            }
            catch (Exception e)
            {
                listBox1.Items.Add(e.Message);
                listBox1.Items.Add(e.StackTrace);
 
                return;
            }
 
            try
            {
                while (true)
                {
                    screen = GetScreen();
                    memoryStream = new MemoryStream();
                    screen.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
                    data = memoryStream.ToArray();
                    pictureBox1.Image = screen;
 
                    if (networkStream.CanWrite)
                    {
                        // 보낼 데이터 사이즈를 서버에 미리 공유
                        dataSizeForServer = BitConverter.GetBytes(data.Length);
                        networkStream.Write(dataSizeForServer, 0, dataSizeForServer.Length);
 
                        // 데이터 송신
                        networkStream.Write(data, 0, data.Length);
                        //listBox1.Items.Add("Data sent: " + (data.Length / 1024).ToString() + "KB");
                    }
 
                    // Thread.Sleep()을 삭제하면 더 부드러운 화면을 볼 수 있다.
                    // 하지만 CPU 점유율이 크게 높아진다. 적절히 조정하자.
                    Thread.Sleep(100);
                }
            }
            catch (Exception e)
            {
                listBox1.Items.Add(e.Message);
                listBox1.Items.Add(e.StackTrace);
            }
 
            client.Close();
            networkStream.Close();
            memoryStream.Close();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            // 클라이언트 접속 대기를 위한 스레드 생성(스레드는 1개만 생성한다)
            if (thread == null || !thread.IsAlive)
            {
                thread = new Thread(new ThreadStart(ThreadProc));
                thread.Start();
                listBox1.Items.Add("Connecting to the server...");
            }
        }
 
        private Bitmap GetScreen()
        {
            Bitmap bitmap = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
            Graphics g = Graphics.FromImage(bitmap);
            g.CopyFromScreen(0000, bitmap.Size);
            g.Dispose();
 
            return bitmap;
        }
 
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (thread != null && thread.IsAlive)
            {
                // 스레드를 종료하지 않으면 백그라운드에서 계속 실행된다.
                thread.Abort();
            }
        }
    }
}
 

 

클라이언트 코드를 입력하고 빌드한다.

Client.zip
0.01MB

 

한 컴퓨터에서 서버, 클라이언트를 모두 실행한 화면

 

 

NetworkStream.DataAvailable 프로퍼티는 네트워크 상황에 따라 부정확한 경우가 많다. 그러므로 서버에서는 클라이언트로 부터 받을 데이터의 사이즈를 미리 확인하고 그 사이즈만큼 정확히 받는게 중요하다.

작은 크기의 데이터는 사이즈 정보 교환없이 한번에 송수신해도 별 문제는 없다. 하지만 이 프로그램처럼 수백 KB의 데이터를 주고 받는 경우 한번에 송수신이 되지 않을 가능성이 크다.

 

클라이언트에서는 서버로 데이터를 전송하기 전, 보낼 데이터 사이즈를 미리 공유해 서버에서 수신 준비할 수 있도록 한다.

MemoryStream으로 저장할 데이터 포맷을 ImageFormat.Jpeg로 바꾸면 ImageFormat.Png에 비해 훨씬 작은 사이즈의 데이터가 만들어지고 영상의 FPS를 향상시킬 수 있다. (PNG는 비손실 압축, JPEG는 손실 압축)

 

반응형
Posted by J-sean
: