还记得刚刚开始接触编程开发时,傻傻的将网站开发和网络编程混为一谈,常常因分不清楚而引为笑柄。后来勉强分清楚,又因为各种各样的协议端口之类的名词而倍感神秘,所以为了揭开网络编程的神秘面纱,本文尝试以一个简单的小例子,简述在网络编程开发中涉及到的相关知识点,仅供学习分享使用,如有不足之处,还请指正。

概述

在tcp/ip协议族中,传输层主要包括tcp和udp两种通信协议,它们以不同的方式实现两台主机中的不同应用程序之间的数据传输,即数据的端到端传输。由于它们的实现方式不同,因此各有一套属于自己的端口号,且相互独立。采用五元组(协议,信源机ip地址,信源应用进程端口,信宿机ip地址,信宿应用进程端口)来描述两个应用进程之间的通信关联,这也是进行网络程序设计最基本的概念。传输控制协议(transmission control protocol,tcp)提供一种面向连接的、可靠的数据传输服务,保证了端到端数据传输的可靠性。

涉及知识点

本例中涉及知识点如下所示:

  1. tcpclient : tcpclient类为tcp网络服务提供客户端连接,它构建于socket类之上,以提供较高级别的tcp服务,提供了通过网络连接、发送和接收数据的简单方法。
  2. tcplistener:构建于socket之上,提供了更高抽象级别的tcp服务,使得程序员能更方便地编写服务器端应用程序。通常情况下,服务器端应用程序在启动时将首先绑定本地网络接口的ip地址和端口号,然后进入侦听客户请求的状态,以便于客户端应用程序提出显式请求。
  3. networkstream:提供网络访问的基础数据流。一旦侦听到有客户端应用程序请求连接侦听端口,服务器端应用将接受请求,并建立一个负责与客户端应用程序通信的信道。

网络聊天示意图

如下图所示:看似两个在不同网络上的人聊天,实际上都是通过服务端进行接收转发的。

tcp网络通信示意图

如下图所示:首先是服务端进行监听,当有客户端进行连接时,则建立通讯通道进行通信。

示例截图

服务端截图,如下所示:

客户端截图,如下所示:开启两个客户端,开始美猴王和二师兄的对话。

核心代码

发送信息类,如下所示:

 1 using system;
 2 using system.collections.generic;
 3 using system.linq;
 4 using system.text;
 5 using system.threading.tasks;
 6 
 7 namespace common
 8 {
 9     /// <summary>
10     /// 定义一个类,所有要发送的内容,都按照这个来
11     /// </summary>
12     public class chatmessage
13     {
14         /// <summary>
15         /// 头部信息
16         /// </summary>
17         public chatheader header { get; set; }
18 
19         /// <summary>
20         /// 信息类型,默认为文本
21         /// </summary>
22         public chattype chattype { get; set; }
23 
24         /// <summary>
25         /// 内容信息
26         /// </summary>
27         public string info { get; set; }
28 
29     }
30 
31     /// <summary>
32     /// 头部信息
33     /// </summary>
34     public class chatheader
35     {
36         /// <summary>
37         /// id唯一标识
38         /// </summary>
39         public string id { get; set; }
40 
41         /// <summary>
42         /// 源:发送方
43         /// </summary>
44         public string source { get; set; }
45 
46         /// <summary>
47         /// 目标:接收方
48         /// </summary>
49         public string dest { get; set; }
50 
51     }
52 
53     /// <summary>
54     /// 内容标识
55     /// </summary>
56     public enum chatmark
57     {
58         begin  = 0x0000,
59         end = 0xffff
60     }
61 
62     public enum chattype {
63         text=0,
64         image=1
65     }
66 }

打包帮助类,如下所示:所有需要发送的信息,都要进行封装,打包,编码成固定格式,方便解析。

 1 using system;
 2 using system.collections.generic;
 3 using system.linq;
 4 using system.text;
 5 using system.threading.tasks;
 6 
 7 namespace common
 8 {
 9     /// <summary>
10     /// 包帮助类
11     /// </summary>
12     public class packhelper
13     {
14         /// <summary>
15         /// 获取待发送的信息
16         /// </summary>
17         /// <param name="text"></param>
18         /// <returns></returns>
19         public static byte[] getsendmsgbytes(string text, string source, string dest)
20         {
21             chatheader header = new chatheader()
22             {
23                 source = source,
24                 dest = dest,
25                 id = guid.newguid().tostring()
26             };
27             chatmessage msg = new chatmessage()
28             {
29                 chattype = chattype.text,
30                 header = header,
31                 info = text
32             };
33             string msg01 = generatepack<chatmessage>(msg);
34             byte[] buffer = encoding.utf8.getbytes(msg01);
35             return buffer;
36         }
37 
38         /// <summary>
39         /// 生成要发送的包
40         /// </summary>
41         /// <typeparam name="t"></typeparam>
42         /// <param name="t"></param>
43         /// <returns></returns>
44         public static string generatepack<t>(t t) {
45             string send = serializerhelper.jsonserialize<t>(t);
46             string res = string.format("{0}|{1}|{2}",chatmark.begin.tostring("x").padleft(4, '0'), send, chatmark.end.tostring("x").padleft(4, '0'));
47             int length = res.length;
48 
49             return string.format("{0}|{1}", length.tostring().padleft(4, '0'), res);
50         }
51 
52         /// <summary>
53         /// 解析包
54         /// </summary>
55         /// <typeparam name="t"></typeparam>
56         /// <param name="receive">原始接收数据包</param>
57         /// <returns></returns>
58         public static t parsepack<t>(string msg, out string error)
59         {
60             error = string.empty;
61             int len = int.parse(msg.substring(0, 4));//传输内容的长度
62             string msg2 = msg.substring(msg.indexof("|") + 1);
63             string[] array = msg2.split('|');
64             if (msg2.length == len)
65             {
66                 string receive = array[1];
67                 string begin = array[0];
68                 string end = array[2];
69                 if (begin == chatmark.begin.tostring("x").padleft(4, '0') && end == chatmark.end.tostring("x").padleft(4, '0'))
70                 {
71                     t t = serializerhelper.jsondeserialize<t>(receive);
72                     if (t != null)
73                     {
74                         return t;
75 
76                     }
77                     else {
78                         error = string.format("接收的数据有误,无法进行解析");
79                         return default(t);
80                     }
81                 }
82                 else {
83                     error = string.format("接收的数据格式有误,无法进行解析");
84                     return default(t);
85                 }
86             }
87             else {
88                 error = string.format("接收数据失败,长度不匹配,定义长度{0},实际长度{1}", len, msg2.length);
89                 return default(t);
90             }
91         }
92     }
93 }

服务端类,如下所示:服务端开启时,需要进行端口监听,等待链接。

 1 using common;
 2 using system;
 3 using system.collections.generic;
 4 using system.configuration;
 5 using system.io;
 6 using system.linq;
 7 using system.net;
 8 using system.net.sockets;
 9 using system.text;
10 using system.threading;
11 using system.threading.tasks;
12 
13 /// <summary>
14 /// 描述:mechat服务端,用于接收数据
15 /// </summary>
16 namespace mechatserver
17 {
18     public class program
19     {
20         /// <summary>
21         /// 服务端ip
22         /// </summary>
23         private static string ip;
24 
25         /// <summary>
26         /// 服务端口
27         /// </summary>
28         private static int port;
29 
30         /// <summary>
31         /// 服务端监听
32         /// </summary>
33         private static tcplistener tcplistener;
34 
35 
36         public static void main(string[] args)
37         {
38             //初始化信息
39             initinfo();
40             ipaddress ipaddr = ipaddress.parse(ip);
41             tcplistener = new tcplistener(ipaddr, port);
42             tcplistener.start();
43           
44             console.writeline("等待连接");
45             tcplistener.beginaccepttcpclient(new asynccallback(asynctcpcallback), "async");
46             //如果用户按下esc键,则结束
47             while (console.readkey().key != consolekey.escape)
48             {
49                 thread.sleep(200);
50             }
51             tcplistener.stop();
52         }
53 
54         /// <summary>
55         /// 初始化信息
56         /// </summary>
57         private static void initinfo() {
58             //初始化服务ip和端口
59             ip = configurationmanager.appsettings["ip"];
60             port = int.parse(configurationmanager.appsettings["port"]);
61             //初始化数据池
62             packpool.tosendlist = new list<chatmessage>();
63             packpool.havesendlist = new list<chatmessage>();
64             packpool.obj = new object();
65         }
66 
67         /// <summary>
68         /// tcp异步接收函数
69         /// </summary>
70         /// <param name="ar"></param>
71         public static void asynctcpcallback(iasyncresult ar) {
72             console.writeline("已经连接");
73             chatlinker linker = new chatlinker(tcplistener.endaccepttcpclient(ar));
74             linker.beginread();
75             //继续下一个连接
76             console.writeline("等待连接");
77             tcplistener.beginaccepttcpclient(new asynccallback(asynctcpcallback), "async");
78         }
79     }
80 }

客户端类,如下所示:客户端主要进行数据的封装发送,接收解析等操作,并在页面关闭时,关闭连接。

  1 using common;
  2 using system;
  3 using system.collections.generic;
  4 using system.componentmodel;
  5 using system.data;
  6 using system.drawing;
  7 using system.linq;
  8 using system.net.sockets;
  9 using system.text;
 10 using system.threading;
 11 using system.threading.tasks;
 12 using system.windows.forms;
 13 
 14 namespace mechatclient
 15 {
 16     /// <summary>
 17     /// 聊天页面
 18     /// </summary>
 19     public partial class frmmain : form
 20     {
 21         /// <summary>
 22         /// 链接客户端
 23         /// </summary>
 24         private tcpclient tcpclient;
 25 
 26         /// <summary>
 27         /// 基础访问的数据流
 28         /// </summary>
 29         private networkstream stream;
 30 
 31         /// <summary>
 32         /// 读取的缓冲数组
 33         /// </summary>
 34         private byte[] bufferread;
 35 
 36         /// <summary>
 37         /// 昵称信息
 38         /// </summary>
 39         private dictionary<string, string> dicnickinfo;
 40 
 41         public frmmain()
 42         {
 43             initializecomponent();
 44         }
 45 
 46         private void mainform_load(object sender, eventargs e)
 47         {
 48             //获取昵称
 49             dicnickinfo = chatinfo.getnickinfo();
 50             //设置标题
 51             string title = string.format(":{0}-->{1} 的对话",dicnickinfo[chatinfo.source], dicnickinfo[chatinfo.dest]);
 52             this.text = string.format("{0}:{1}", this.text, title);
 53             //初始化客户端连接
 54             this.tcpclient = new tcpclient(addressfamily.internetwork);
 55             bufferread = new byte[this.tcpclient.receivebuffersize];
 56             this.tcpclient.beginconnect(chatinfo.ip, chatinfo.port, new asynccallback(requestcallback), null);
 57           
 58         }
 59 
 60         /// <summary>
 61         /// 异步请求链接函数
 62         /// </summary>
 63         /// <param name="ar"></param>
 64         private void requestcallback(iasyncresult ar) {
 65             this.tcpclient.endconnect(ar);
 66             this.lblstatus.text = "连接服务器成功";
 67             //获取流
 68             stream = this.tcpclient.getstream();
 69             //先发送一个连接信息
 70             string text = commonvar.login;
 71             byte[] buffer = packhelper.getsendmsgbytes(text,chatinfo.source,chatinfo.source);
 72             stream.beginwrite(buffer, 0, buffer.length, new asynccallback(writemessage), null);
 73             //只有stream不为空的时候才可以读
 74             stream.beginread(bufferread, 0, bufferread.length, new asynccallback(readmessage), null);
 75         }
 76 
 77         /// <summary>
 78         /// 发送信息
 79         /// </summary>
 80         /// <param name="sender"></param>
 81         /// <param name="e"></param>
 82         private void btnsend_click(object sender, eventargs e)
 83         {
 84             string text = this.txtmsg.text.trim();
 85             if( string.isnullorempty(text)){
 86                 messagebox.show("要发送的信息为空");
 87                 return;
 88             }
 89             byte[] buffer = chatinfo.getsendmsgbytes(text);
 90             stream.beginwrite(buffer, 0, buffer.length, new asynccallback(writemessage), null);
 91             this.rtallmsg.appendtext(string.format("\r\n[{0}]", dicnickinfo[chatinfo.source]));
 92             this.rtallmsg.selectionalignment = horizontalalignment.right;
 93             this.rtallmsg.appendtext(string.format("\r\n{0}", text));
 94             this.rtallmsg.selectionalignment = horizontalalignment.right;
 95         }
 96 
 97     
 98         /// <summary>
 99         /// 异步读取信息
100         /// </summary>
101         /// <param name="ar"></param>
102         private void readmessage(iasyncresult ar)
103         {
104             if (stream.canread)
105             {
106                 int length = stream.endread(ar);
107                 if (length >= 1)
108                 {
109 
110                     string msg = string.empty;
111                     msg = string.concat(msg, encoding.utf8.getstring(bufferread, 0, length));
112                     //处理接收的数据
113                     string error = string.empty;
114                     chatmessage t = packhelper.parsepack<chatmessage>(msg, out error);
115                     if (string.isnullorempty(error))
116                     {
117                         this.rtallmsg.invoke(new action(() =>
118                         {
119                             this.rtallmsg.appendtext(string.format("\r\n[{0}]", dicnickinfo[t.header.source]));
120                             this.rtallmsg.selectionalignment = horizontalalignment.left;
121                             this.rtallmsg.appendtext(string.format("\r\n{0}", t.info));
122                             this.rtallmsg.selectionalignment = horizontalalignment.left;
123                             this.lblstatus.text = "接收数据成功!";
124                         }));
125                     }
126                     else {
127                         this.lblstatus.text = "接收数据失败:"+error;
128                     }
129                 }
130                 //继续读数据
131                 stream.beginread(bufferread, 0, bufferread.length, new asynccallback(readmessage), null);
132             }
133         }
134 
135         /// <summary>
136         /// 发送成功
137         /// </summary>
138         /// <param name="ar"></param>
139         private void writemessage(iasyncresult ar)
140         {
141             this.stream.endwrite(ar);
142             //发送成功
143         }
144 
145         /// <summary>
146         /// 页面关闭,断开连接
147         /// </summary>
148         /// <param name="sender"></param>
149         /// <param name="e"></param>
150         private void frmmain_formclosing(object sender, formclosingeventargs e)
151         {
152             if (messagebox.show("正在通话中,确定要关闭吗?", "关闭", messageboxbuttons.yesno) == dialogresult.yes)
153             {
154                 e.cancel = false;
155                 string text = commonvar.quit;
156                 byte[] buffer = chatinfo.getsendmsgbytes(text);
157                 stream.write(buffer, 0, buffer.length);
158                 //发送完成后,关闭连接
159                 this.tcpclient.close();
160 
161             }
162             else {
163                 e.cancel = true;
164             }
165         }
166     }
167 }

备注:本示例中,所有的建立连接,数据接收,发送等都是采用异步方式,防止页面卡顿。

备注

每一次的努力,都是幸运的伏笔。