【课题】:【基于网络端口通信的客户端应用程序】解决方案【示意程序】
【创建日期】:2007年6月7日星期四
【完成日期】:2007年6月8日星期五
【级别】:Demo示例程序
【关键字】:Socket,TCP/IP,UDP,WinForm,Net,C#
【解决方案说明】:
本解决方案用于描述基于网络端口通信的客户端应用程序解决方案。本示例仅为了展示应用程序如何利用端口进行通信。本解决方案包含两个项目(TcpServerClient,UdpServerClient)分别用于展示TCP/IP协议下的强制连接通信过程和UDP协议下的无连接通信过程。两种过程相互独立,可以根据应用程序的具体使用环境而自由选择。
程序均通过自身接收和发送文本消息。所有消息通过端口进行通信。监听消息的部分均采用单独线程进行工作(多线程),以获得发送消息的自由和不间断的消息监听机制。
TCP/IP协议版本可以在单机开启两个应用程序,但是其中一个将显示异常报告,两个窗体进行的输入将在另外一个单独的正常的窗体中显示。UDP协议版本则不可以在单机开启两个应用程序。但这并不表示应用程序之间无法自由通信,原因是同一应用程序对同一计算机的同一端口进行监听将会出现异常,这部分内容有待完善。
本应用程序可在局域网内自由通信,若通信失败,请先检查是否开启了防火墙拦截了此软件的通信。
【程序截图】:
【程序解读】:
C#:(仅以TCP进行代码讲解,UDP协议的基本思路相同)
// 以下代码片段展示了ChatTool.cs中的代码,描述了整个应用程序的通信机制。
1
using
System;
2
using
System.Text;
3
using
System.Windows.Forms;
4
using
System.Net;
5
using
System.Net.Sockets;
6
using
System.Threading;
7
8
namespace
TcpServerClient
9
{
10 public partial class ChatTool : Form
11 {
12 //声明一个监听者
13 TcpListener listener;
14
15 //指定发送和接收端口
16 //单机使用,发送接收端口必须一致
17 int sendPort = 11000;
18 int receivePort = 11000;
19
20 public ChatTool()
21 {
22 InitializeComponent();
23 }
24
25 private void Client_Load(object sender, EventArgs e)
26 {
27
28 //当窗体加载的时候创建一个新的线程
29 //新的线程负责监听者的动作Listen()
30 //线程语法:
31 //new Thread(new ThreadStart(具体你所要委托线程执行的目标程序))
32 //如此处的Listen
33 Thread workTicketThread = new Thread(new ThreadStart(Listen));
34 workTicketThread.IsBackground = true;
35 workTicketThread.Start();
36 }
37
38 private void btnSend_Click(object sender, EventArgs e)
39 {
40 //按下发送按钮的时候触发该事件
41 //将窗体中的数据发送出去
42 Send(txtSpeaker.Text);
43 //将窗体中的内容清空
44 txtSpeaker.Clear();
45 }
46
47 private IPAddress GetIPAddress()
48 {
49 IPAddress ip;
50
51 //尝试转换IP框中的数据,由于是一个字符串
52 //因此首先需要判断它是否可以转换为字符串
53 //如果不可以则转换为127.0.0.1,代表本机环回IP地址
54 //out语法:在参数前使用out,表示该参数是在外部定义的(像这里的ip,是我们在外部进行定义的)
55 // 而该参数经过该函数(这里指TryParse)的执行后,值将由该参数在函数内的执行结果为主
56 if (IPAddress.TryParse(txtIPAddress.Text, out ip) == false)
57 IPAddress.TryParse("127.0.0.1", out ip);
58 return ip;
59 }
60
61 private void Listen()
62 {
63 //设置Listen方法收取的
64 int bufferSize = 1024;
65
66 try
67 {
68 //监听者在机器的指定IP地址和端口进行侦听
69 listener = new TcpListener(IPAddress.Any, receivePort);
70 //只有在运行了Start之后才正式开始侦听
71 listener.Start();
72
73 //始终监听指定IP地址和端口
74 while (true)
75 {
76 //监听者只有在运行AcceptTcpClient,就进入挂起的连接请求
77 //注意:程序运行到这里的时候,就会停顿在这里进行监听,
78 // 后面的工作将被放在接收到TCP连接之后继续执行。
79 // 这也就是为什么要采用单独线程来处理监听事件的缘故了。
80 // 否则程序在一开始就会停顿在这个位置,甚至无法初始化
81 //问题:可以尝试不使用多线程进行监听,看看会发生什么效果?
82 TcpClient client = listener.AcceptTcpClient();
83
84 //来自TcpClient的消息可以用GetStream来接收
85 NetworkStream clientStream = client.GetStream();
86
87 //将接收到的流转换成本地流
88 //约定:这里采用Unicode编码方式在二进制和文本之间进行转换
89 byte[] buffer = new byte[bufferSize];
90 int readBytes = 0;
91 readBytes = clientStream.Read(buffer, 0, bufferSize);
92 //将接收到的流还原为文本
93 string request = Encoding.Unicode.GetString(buffer).Substring(0, readBytes);
94
95 //将接收到的文本格式化输出
96 txtMessage.Text = ReceiveMessageFormat(txtMessage.Text, request);
97
98 //注意:在使用完流之后要将其显式关闭,以让垃圾回收器对其进行回收
99 clientStream.Close();
100 }
101 }
102 catch (Exception ex)
103 {
104 txtMessage.Text = ex.ToString();
105 }
106 finally
107 {
108 //每次监听完毕需要关闭监听者,以保证线程可以被垃圾回收器回收
109 //垃圾回收器知识:.NET自带的垃圾回收器机制允许调用者不必关心对象的内存释放问题
110 // 当垃圾回收器检测到该资源没有用的时候,将会对其内存进行释放
111 // 假设这里不停止监听者的工作,那么该线程将一直处于忙状态,垃圾回收器判断
112 // 该线程仍然在使用,这时它将得不到释放。
113 // 这样做的后果将是:当整个应用程序的主线程(窗体所在的线程)关闭的时候,
114 // 也就是窗体关闭的时候,我们希望属于这个应用程序的所有内存得到释放
115 // 但是垃圾回收器没有对它进行回收,也就是它仍然工作着。
116 // 这时候就发生了内存泄漏。这种现象严重的可能导致操作系统的崩溃。
117 listener.Stop();
118 }
119 }
120
121 private void Send(string message)
122 {
123 TcpClient client = new TcpClient();
124 try
125 {
126 //获取要发送的IP地址
127 IPAddress ip = GetIPAddress();
128 //通过IP地址获得实体,可用于获取与主机相关的信息,这里主要为了获得主机名
129 IPHostEntry host = Dns.GetHostEntry(ip);
130
131 //使用指定的IP地址和端口号将本地主机连接到远程主机
132 //TCP知识:TCP是建立连接的网络传输协议,需要在通信前经过握手连接以保证二者之间的连通性。
133 client.Connect(ip, sendPort);
134
135 //将要传输的文本转换成流,将其写入,流将通过网络传输到远程主机,
136 //远程主机经过监听,将流转换成本地文本进行显示
137 NetworkStream clientStream = client.GetStream();
138 string request = SendMessageFormat(host.HostName, message);
139 byte[] requestBuffer = Encoding.Unicode.GetBytes(request);
140 clientStream.Write(requestBuffer, 0, requestBuffer.Length);
141 }
142 catch (System.Net.Sockets.SocketException)
143 {
144 txtMessage.Text = "请检查对方接收方应用程序是否开启!如果没有,请将其开启,TCP/IP应用程序要求接收方处于监听状态!";
145 }
146 finally
147 {
148 client.Close();
149 }
150 }
151
152 private string SendMessageFormat(string hostName,string message)
153 {
154 return string.Format("{0}{1}来自{2}的消息:{1}{1}{3}{1}{1}", DateTime.Now, Environment.NewLine, hostName, message);
155 }
156
157 private string ReceiveMessageFormat(string oldMessage, string newMessage)
158 {
159 string tempMessage = "";
160 if (oldMessage.Trim() == string.Empty)
161 tempMessage = newMessage;
162 else
163 tempMessage = oldMessage + Environment.NewLine + newMessage;
164 return tempMessage;
165 }
166
167 private void Client_FormClosing(object sender, FormClosingEventArgs e)
168 {
169 //如果不调用关闭监听,则listener仍然处于监听状态,
170 //应用程序的主窗体虽然关闭,但是在后台进程中依然
171 //保留,这将导致再次启动的时候会报错!
172 listener.Stop();
173 }
174 }
175}
176
注意:以上程序并非线程安全的,如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的 bug,包括争用情况和死锁。确保以线程安全方式访问控件非常重要。(本程序在XP、2003下运行均通过,TCP版同样能在Vista版本下通过,而UDP版则在Vista下因为线程安全的问题报错。) 解决方案1:判断窗体InvokeRequired,如果值为true则利用委托进行调用。 解决方案2:使用 BackgroundWorker 进行的线程安全调用(可以在工具箱中找到该控件) |
1
//
以下代码片段为窗体设计代码
2
namespace
TcpServerClient
3
{
4 partial class ChatTool
5 {
6 /**//// <summary>
7 /// 必需的设计器变量。
8 /// </summary>
9 private System.ComponentModel.IContainer components = null;
10
11 /**//// <summary>
12 /// 清理所有正在使用的资源。
13 /// </summary>
14 /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
15 protected override void Dispose(bool disposing)
16 {
17 if (disposing && (components != null))
18 {
19 components.Dispose();
20 }
21 base.Dispose(disposing);
22 }
23
24 Windows 窗体设计器生成的代码#region Windows 窗体设计器生成的代码
25
26 /**//// <summary>
27 /// 设计器支持所需的方法 - 不要
28 /// 使用代码编辑器修改此方法的内容。
29 /// </summary>
30 private void InitializeComponent()
31 {
32 System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ChatTool));
33 this.btnSend = new System.Windows.Forms.Button();
34 this.txtIPAddress = new System.Windows.Forms.TextBox();
35 this.txtSpeaker = new System.Windows.Forms.TextBox();
36 this.txtMessage = new System.Windows.Forms.RichTextBox();
37 this.SuspendLayout();
38 //
39 // btnSend
40 //
41 this.btnSend.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
42 this.btnSend.Location = new System.Drawing.Point(377, 247);
43 this.btnSend.Name = "btnSend";
44 this.btnSend.Size = new System.Drawing.Size(75, 23);
45 this.btnSend.TabIndex = 0;
46 this.btnSend.Text = "Send";
47 this.btnSend.UseVisualStyleBackColor = true;
48 this.btnSend.Click += new System.EventHandler(this.btnSend_Click);
49 //
50 // txtIPAddress
51 //
52 this.txtIPAddress.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
53 | System.Windows.Forms.AnchorStyles.Right)));
54 this.txtIPAddress.Location = new System.Drawing.Point(1, 247);
55 this.txtIPAddress.Name = "txtIPAddress";
56 this.txtIPAddress.Size = new System.Drawing.Size(356, 21);
57 this.txtIPAddress.TabIndex = 2;
58 this.txtIPAddress.Text = "127.0.0.1";
59 //
60 // txtSpeaker
61 //
62 this.txtSpeaker.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
63 | System.Windows.Forms.AnchorStyles.Right)));
64 this.txtSpeaker.Location = new System.Drawing.Point(1, 183);
65 this.txtSpeaker.Multiline = true;
66 this.txtSpeaker.Name = "txtSpeaker";
67 this.txtSpeaker.Size = new System.Drawing.Size(462, 58);
68 this.txtSpeaker.TabIndex = 3;
69 //
70 // txtMessage
71 //
72 this.txtMessage.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
73 | System.Windows.Forms.AnchorStyles.Left)
74 | System.Windows.Forms.AnchorStyles.Right)));
75 this.txtMessage.AutoWordSelection = true;
76 this.txtMessage.Location = new System.Drawing.Point(1, 0);
77 this.txtMessage.Name = "txtMessage";
78 this.txtMessage.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical;
79 this.txtMessage.Size = new System.Drawing.Size(462, 177);
80 this.txtMessage.TabIndex = 4;
81 this.txtMessage.Text = "";
82 //
83 // ChatTool
84 //
85 this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
86 this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
87 this.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("$this.BackgroundImage")));
88 this.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Center;
89 this.ClientSize = new System.Drawing.Size(464, 282);
90 this.Controls.Add(this.txtMessage);
91 this.Controls.Add(this.txtSpeaker);
92 this.Controls.Add(this.txtIPAddress);
93 this.Controls.Add(this.btnSend);
94 this.Name = "ChatTool";
95 this.Opacity = 0.9;
96 this.Text = "TcpVersion";
97 this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Client_FormClosing);
98 this.Load += new System.EventHandler(this.Client_Load);
99 this.ResumeLayout(false);
100 this.PerformLayout();
101
102 }
103
104 #endregion
105
106 private System.Windows.Forms.Button btnSend;
107 private System.Windows.Forms.TextBox txtIPAddress;
108 private System.Windows.Forms.TextBox txtSpeaker;
109 private System.Windows.Forms.RichTextBox txtMessage;
110 }
111}
112
113
114
115
/**/
/// <summary>
116/// 应用程序的主入口点。
117/// </summary>
118
[STAThread]
119
static
void
Main()
120
{
121 Application.EnableVisualStyles();
122 Application.SetCompatibleTextRenderingDefault(false);
123 Application.Run(new ChatTool());
124}
125
【免责声明】:
本解决方案只为示意如何进行简单的Socket编程,程序中可能存在有明显的未修复BUG,不影响示意程序的正常表达,可能未作修复。如同一计算机开启两个相同窗体可能引发的异常等。
【欢迎交流】:
与本示意有关的任何问题,请与我取得联系,我将积极为您解答。