Socket 由浅入深,开发一个真正的通信应用

在说socket之前。我们先了解下相关的网络知识;

端口

 在Internet上有很多这样的主机,这些主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序)。

例如:http 使用80端口 ftp使用21端口 smtp使用 25端口

端口用来标识计算机里的某个程序   1)公认端口:从0到1023   2)注册端口:从1024到49151   3)动态或私有端口:从49152到65535

 

Socket相关概念

socket的英文原义是“孔”或“插座”。作为进程通信机制,取后一种意思。通常也称作“套接字”,用于描述IP地址和端口,是一个通信链的句柄。(其实就是两个程序通信用的。)

socket非常类似于电话插座。以一个电话网为例。电话的通话双方相当于相互通信的2个程序,电话号码就是IP地址。任何用户在通话之前,

首先要占有一部电话机,相当于申请一个socket;同时要知道对方的号码,相当于对方有一个固定的socket。然后向对方拨号呼叫,

相当于发出连接请求。对方假如在场并空闲,拿起电话话筒,双方就可以正式通话,相当于连接成功。双方通话的过程,

是一方向电话机发出信号和对方从电话机接收信号的过程,相当于向socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭socket,撤消连接。

 

Socket有两种类型

流式Socket(STREAM): 是一种面向连接的Socket,针对于面向连接的TCP服务应用,安全,但是效率低;

数据报式Socket(DATAGRAM): 是一种无连接的Socket,对应于无连接的UDP服务应用.不安全(丢失,顺序混乱,在接收端要分析重排及要求重发),但效率高.

 

TCP/IP协议

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

UDP协议

UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。

应用层 (Application):应用层是个很广泛的概念,有一些基本相同的系统级 TCP/IP 应用以及应用协议,也有许多的企业商业应用和互联网应用。 解释:我们的应用程序

传输层 (Transport):传输层包括 UDP 和 TCP,UDP 几乎不对报文进行检查,而 TCP 提供传输保证。 解释;保证传输数据的正确性

网络层 (Network):网络层协议由一系列协议组成,包括 ICMP、IGMP、RIP、OSPF、IP(v4,v6) 等。 解释:保证找到目标对象,因为里面用的IP协议,ip包含一个ip地址

链路层 (Link):又称为物理数据网络接口层,负责报文传输。 解释:在物理层面上怎么去传递数据

 

你可以cmd打开命令窗口。输入

netstat -a

查看当前电脑监听的端口,和协议。有TCP和UDP

 

 

TCP/IP与UDP有什么区别呢?该怎么选择?

  UDP可以用广播的方式。发送给每个连接的用户   而TCP是做不到的

  TCP需要3次握手,每次都会发送数据包(但不是我们想要发送的数据),所以效率低   但数据是安全的。因为TCP会有一个校验和。就是在发送的时候。会把数据包和校验和一起   发送过去。当校验和和数据包不匹配则说明不安全(这个安全不是指数据会不会   别窃听,而是指数据的完整性)

  UDP不需要3次握手。可以不发送校验和

  web服务器用的是TCP协议

那什么时候用UDP协议。什么时候用TCP协议呢?   视频聊天用UDP。因为要保证速度?反之相反

   

下图显示了数据报文的格式

 

 

Socket一般应用模式(服务器端和客户端)

 

 

服务端跟客户端发送信息的时候,是通过一个应用程序 应用层发送给传输层,传输层加头部 在发送给网络层。在加头 在发送给链路层。在加帧

 

然后在链路层转为信号,通过ip找到电脑 链路层接收。去掉头(因为发送的时候加头了。去头是为了找到里面的数据) 网络层接收,去头 传输层接收。去头 在到应用程序,解析协议。把数据显示出来

 

TCP3次握手

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。   第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;SYN:同步序列编号(Synchronize SequenceNumbers)。   第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;   第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

 

 

看一个Socket简单的通信图解

 

 

 

1.服务端welcoming socket 开始监听端口(负责监听客户端连接信息)

2.客户端client socket连接服务端指定端口(负责接收和发送服务端消息)

3.服务端welcoming socket 监听到客户端连接,创建connection socket。(负责和客户端通信)

 

服务器端的Socket(至少需要两个)

一个负责接收客户端连接请求(但不负责与客户端通信)

每成功接收到一个客户端的连接便在服务端产生一个对应的负责通信的Socket 在接收到客户端连接时创建. 为每个连接成功的客户端请求在服务端都创建一个对应的Socket(负责和客户端通信).

客户端的Socket

客户端Socket 必须指定要连接的服务端地址和端口。 通过创建一个Socket对象来初始化一个到服务器端的TCP连接。

 

 

Socket的通讯过程

服务器端:

申请一个socket 绑定到一个IP地址和一个端口上 开启侦听,等待接授连接

客户端: 申请一个socket 连接服务器(指明IP地址和端口号)

服务器端接到连接请求后,产生一个新的socket(端口大于1024)与客户端建立连接并进行通讯,原监听socket继续监听。

 

 

socket是一个很抽象的概念。来看看socket的位置

 

好吧。我承认看一系列的概念是非常痛苦的,现在开始编码咯

 

看来编码前还需要看下sokcet常用的方法

Socket方法: 1)IPAddress类:包含了一个IP地址 例:IPAddress  ip = IPAddress.Parse(txtServer.Text);//将IP地址字符串转换后赋给ip 2) IPEndPoint类:包含了一对IP地址和端口号 例:IPEndPoint point = new IPEndPoint(ip, int.Parse(txtPort.Text));//将指定的IP地址和端口初始化后赋给point 3)Socket (): 创建一个Socket 例:Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//创建监听用的socket 4) Bind(): 绑定一个本地的IP和端口号(IPEndPoint) 例:socket.Bind(point);//绑定ip和端口 5) Listen(): 让Socket侦听传入的连接尝试,并指定侦听队列容量 例: socket.Listen(10); 6) Connect(): 初始化与另一个Socket的连接 7) Accept(): 接收连接并返回一个新的socket 例:Socket connSocket =socket .Accept (); 8 )Send(): 输出数据到Socket 9) Receive(): 从Socket中读取数据 10) Close(): 关闭Socket (销毁连接)

 

首先创建服务端,服务端是用来监听客户端请求的。

创建服务器步骤:   第一步:创建一个Socket,负责监听客户端的请求,此时会监听一个端口   第二步:客户端创建一个Socket去连接服务器的ip地址和端口号   第三步:当连接成功后。会创建一个新的socket。来负责和客户端通信

复制代码
 1 public static void startServer()
 2         {
 3 
 4             //第一步:创建监听用的socket
 5             Socket socket = new Socket
 6             (
 7                 AddressFamily.InterNetwork, //使用ip4
 8                 SocketType.Stream,//流式Socket,基于TCP
 9                 ProtocolType.Tcp //tcp协议
10             );
11 
12             //第二步:监听的ip地址和端口号
13             //ip地址
14             IPAddress ip = IPAddress.Parse(_ip);
15             //ip地址和端口号
16             IPEndPoint point = new IPEndPoint(ip, _point);
17 
18             //绑定ip和端口
19             //端口号不能占用:否则:以一种访问权限不允许的方式做了一个访问套接字的尝试
20             //通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
21             try
22             {
23                 socket.Bind(point);
24             }
25             catch (Exception)
26             {
27 
28                 if (new IOException().InnerException is SocketException)
29                     Console.WriteLine("端口被占用");
30             }
31             //socket.Bind(point);
32 
33             //第三步:开始监听端口
34 
35             //监听队列的长度
36             /*比如:同时有3个人来连接该服务器,因为socket同一个时间点。只能处理一个连接
37              * 所以其他的就要等待。当处理第一个。然后在处理第二个。以此类推
38              * 
39              * 这里的10就是同一个时间点等待的队列长度为10,即。只能有10个人等待,当第11个的时候。是连接不上的
40              */
41             socket.Listen(10);
42 
43             string msg = string.Format("服务器已经启动........\n监听ip为:{0}\n监听端口号为:{1}\n", _ip, _point);
44             showMsg(msg);
45 
46             Thread listen = new Thread(Listen);
47             listen.IsBackground = true;
48             listen.Start(socket);
49 
50         }
复制代码

 

 

观察上面的代码。开启了一个多线程。去执行Listen方法,Listen是什么?为什么要开启一个多线程去执行?

回到上面的 "Socket的通讯过程"中提到的那个图片,因为有两个地方需要循环执行

第一个:需要循环监听来自客户端的请求

第二个:需要循环获取来自客服端的通信(这里假设是客户端跟服务器聊天)

额。这跟使用多线程有啥关系?当然有。因为Accept方法。会阻塞线程。所以用多线程,避免窗体假死。你说呢?

看看Listen方法

复制代码
 1 /// <summary>
 2         /// 多线程执行
 3         /// Accept方法。会阻塞线程。所以用多线程
 4         /// </summary>
 5         /// <param name="o"></param>
 6         static void Listen(object o)
 7         {
 8             Socket socket = o as Socket;
 9 
10             //不停的接收来自客服端的连接
11             while (true)
12             {
13                 //如果有客服端连接,则创建通信用是socket  
14                 //Accept方法。会阻塞线程。所以用多线程
15                 //Accept方法会一直等待。直到有连接过来
16                 Socket connSocket = socket.Accept();
17 
18                 //获取连接成功的客服端的ip地址和端口号
19                 string msg = connSocket.RemoteEndPoint.ToString();
20                 showMsg(msg + "连接");
21 
22                 //获取本机的ip地址和端口号
23                 //connSocket.LocalEndPoint.ToString();
24 
25                 /*
26                  如果不用多线程。则会一直执行ReceiveMsg
27                  * 就不会接收客服端连接了
28                  */
29                 Thread th = new Thread(ReceiveMsg);
30                 th.IsBackground = true;
31                 th.Start(connSocket);
32 
33             }
34         }
复制代码

 

细心的你在Listen方法底部又看到了一个多线程。执行ReceiveMsg,对,没错。这就是上面说的。循环获取消息

ReceiveMsg方法定义:

复制代码
 1  /// <summary>
 2         /// 接收数据
 3         /// </summary>
 4         /// <param name="o"></param>
 5         static void ReceiveMsg(object o)
 6         {
 7             Socket connSocket = o as Socket;
 8             while (true)
 9             {
10 
11                 //接收数据
12                 byte[] buffer = new byte[1024 * 1024];//1M
13                 int num = 0;
14                 try
15                 {
16                     //接收数据保存发送到buffer中
17                     //num则为实际接收到的字节个数
18 
19                     //这里会遇到这个错误:远程主机强迫关闭了一个现有的连接。所以try一下
20                     num = connSocket.Receive(buffer);
21                     //当num=0.说明客服端已经断开
22                     if (num == 0)
23                     {
24                         connSocket.Shutdown(SocketShutdown.Receive);
25                         connSocket.Close();
26                         break;
27                     }
28                 }
29                 catch (Exception ex)
30                 {
31                     if (new IOException().InnerException is SocketException)
32                         Console.WriteLine("网络中断");
33                     else
34                         Console.WriteLine(ex.Message);
35                     break;
36                 }
37 
38                 //把实际有效的字节转化成字符串
39                 string str = Encoding.UTF8.GetString(buffer, 0, num);
40                 showMsg(connSocket.RemoteEndPoint + "说:\n" + str);
41 
42 
43 
44             }
45         }
复制代码

 

提供服务器的完整代码如下:

复制代码
  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Net.Sockets;
  6 using System.Net;
  7 using System.Threading;
  8 using System.IO;
  9 namespace CAServer
 10 {
 11     class Program
 12     {
 13 
 14         //当前主机ip
 15         static string _ip = "192.168.1.2";
 16         //端口号
 17         static int _point = 8000;
 18 
 19         static void Main(string[] args)
 20         {
 21             //Thread thread = new Thread(startServer);
 22             //thread.Start();
 23 
 24             startServer();
 25 
 26             Console.ReadLine();
 27 
 28         }
 29 
 30         public static void startServer()
 31         {
 32 
 33             //第一步:创建监听用的socket
 34             Socket socket = new Socket
 35             (
 36                 AddressFamily.InterNetwork, //使用ip4
 37                 SocketType.Stream,//流式Socket,基于TCP
 38                 ProtocolType.Tcp //tcp协议
 39             );
 40 
 41             //第二步:监听的ip地址和端口号
 42             //ip地址
 43             IPAddress ip = IPAddress.Parse(_ip);
 44             //ip地址和端口号
 45             IPEndPoint point = new IPEndPoint(ip, _point);
 46 
 47             //绑定ip和端口
 48             //端口号不能占用:否则:以一种访问权限不允许的方式做了一个访问套接字的尝试
 49             //通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
 50             try
 51             {
 52                 socket.Bind(point);
 53             }
 54             catch (Exception)
 55             {
 56 
 57                 if (new IOException().InnerException is SocketException)
 58                     Console.WriteLine("端口被占用");
 59             }
 60             //socket.Bind(point);
 61 
 62             //第三步:开始监听端口
 63 
 64             //监听队列的长度
 65             /*比如:同时有3个人来连接该服务器,因为socket同一个时间点。只能处理一个连接
 66              * 所以其他的就要等待。当处理第一个。然后在处理第二个。以此类推
 67              * 
 68              * 这里的10就是同一个时间点等待的队列长度为10,即。只能有10个人等待,当第11个的时候。是连接不上的
 69              */
 70             socket.Listen(10);
 71 
 72             string msg = string.Format("服务器已经启动........\n监听ip为:{0}\n监听端口号为:{1}\n", _ip, _point);
 73             showMsg(msg);
 74 
 75             Thread listen = new Thread(Listen);
 76             listen.IsBackground = true;
 77             listen.Start(socket);
 78 
 79         }
 80         /// <summary>
 81         /// 多线程执行
 82         /// Accept方法。会阻塞线程。所以用多线程
 83         /// </summary>
 84         /// <param name="o"></param>
 85         static void Listen(object o)
 86         {
 87             Socket socket = o as Socket;
 88 
 89             //不停的接收来自客服端的连接
 90             while (true)
 91             {
 92                 //如果有客服端连接,则创建通信用是socket  
 93                 //Accept方法。会阻塞线程。所以用多线程
 94                 //Accept方法会一直等待。直到有连接过来
 95                 Socket connSocket = socket.Accept();
 96 
 97                 //获取连接成功的客服端的ip地址和端口号
 98                 string msg = connSocket.RemoteEndPoint.ToString();
 99                 showMsg(msg + "连接");
100 
101                 //获取本机的ip地址和端口号
102                 //connSocket.LocalEndPoint.ToString();
103 
104                 /*
105                  如果不用多线程。则会一直执行ReceiveMsg
106                  * 就不会接收客服端连接了
107                  */
108                 Thread th = new Thread(ReceiveMsg);
109                 th.IsBackground = true;
110                 th.Start(connSocket);
111 
112             }
113         }
114         /// <summary>
115         /// 接收数据
116         /// </summary>
117         /// <param name="o"></param>
118         static void ReceiveMsg(object o)
119         {
120             Socket connSocket = o as Socket;
121             while (true)
122             {
123 
124                 //接收数据
125                 byte[] buffer = new byte[1024 * 1024];//1M
126                 int num = 0;
127                 try
128                 {
129                     //接收数据保存发送到buffer中
130                     //num则为实际接收到的字节个数
131 
132                     //这里会遇到这个错误:远程主机强迫关闭了一个现有的连接。所以try一下
133                     num = connSocket.Receive(buffer);
134                     //当num=0.说明客服端已经断开
135                     if (num == 0)
136                     {
137                         connSocket.Shutdown(SocketShutdown.Receive);
138                         connSocket.Close();
139                         break;
140                     }
141                 }
142                 catch (Exception ex)
143                 {
144                     if (new IOException().InnerException is SocketException)
145                         Console.WriteLine("网络中断");
146                     else
147                         Console.WriteLine(ex.Message);
148                     break;
149                 }
150 
151                 //把实际有效的字节转化成字符串
152                 string str = Encoding.UTF8.GetString(buffer, 0, num);
153                 showMsg(connSocket.RemoteEndPoint + "说:\n" + str);
154 
155 
156 
157             }
158         }
159         /// <summary>
160         /// 显示消息
161         /// </summary>
162         static void showMsg(string msg)
163         {
164             Console.WriteLine(msg);
165             //Console.ReadKey();
166         }
167     }
168 }
复制代码

 

运行代码。显示如下

是不是迫不及待的想试试看效果。好吧其实我也跟你一样,cmd打开dos命令提示符,输入

telnet  192.168.1.2 8000

回车,会看到窗体名称变了

 

然后看到服务器窗口

然后在客户端输入数字试试

我输入了1 2 3 。当然,在cmd窗口是不显示的。这不影响测试。

小技巧:为了便于测试,可以创建一个xx.bat文件。里面写命令

telnet  192.168.1.2 8000

这样只有每次打开就会自动连接了。

当然。这仅仅是测试。现在写一个客户端,

创建一个winfrom程序,布局如下显示

请求服务器代码就很容易了。直接附上代码

复制代码
 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel;
 4 using System.Data;
 5 using System.Drawing;
 6 using System.Linq;
 7 using System.Text;
 8 using System.Windows.Forms;
 9 using System.Net;
10 using System.Net.Sockets;
11 
12 namespace WFAClient
13 {
14     public partial class Form1 : Form
15     {
16         public Form1()
17         {
18             InitializeComponent();
19         }
20         Socket socket;
21         private void btnOk_Click(object sender, EventArgs e)
22         {
23             //客户端连接IP
24             IPAddress ip = IPAddress.Parse(tbIp.Text);
25 
26             //端口号
27             IPEndPoint point = new IPEndPoint(ip, int.Parse(tbPoint.Text));
28 
29             socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
30 
31             try
32             {
33                 socket.Connect(point);
34                 msg("连接成功");
35                 btnOk.Enabled = false;
36             }
37             catch (Exception ex)
38             {
39                 msg(ex.Message);
40             }
41         }
42         private void msg(string msg)
43         {
44             tbMsg.AppendText(msg);
45 
46         }
47 
48         private void btnSender_Click(object sender, EventArgs e)
49         {
50             //发送信息
51             if (socket != null)
52             {
53                 byte[] buffer = Encoding.UTF8.GetBytes(tbContent.Text);
54                 socket.Send(buffer);
55                 /*
56                  * 如果不释放资源。当关闭连接的时候
57                  * 服务端接收消息会报如下异常:
58                  * 远程主机强迫关闭了一个现有的连接。
59                  */
60                 //socket.Close();
61                 //socket.Disconnect(true);
62             }
63         }
64     }
65 }
复制代码

 

运行测试,这里需要同时运行客户端和服务器,

首先运行服务器,那怎么运行客户端呢。

右键客户端项目。调试--》启用新实例

 

 

 

好了。一个入门的过程就这样悄悄的完成了。

以上内容来自:http://www.cnblogs.com/nsky/p/4501782.html

 

根据上面的内容,已经可以开发出一个可以正常通信的Socket示例了,

接下来首先要考虑的就是服务器性能问题

1)在服务器接收数据的时候,定义了一个1M的Byte Buffer,有些设计的更大。更大Buffer可以保证客户端发送数据量很大的情况全部能接受完全。但是作为一个服务器每收到一条客户端请求,都要申请一个1M的Buffer去装客户端发送的数据。如果客户端的并发量很大的情况,还没等到网络的瓶颈,服务器内存开销已经吃不消了。

对于这个问题的解决思路是:

定义一个小Buffer,每次接受客户端请求用:

byte[] bufferTemp = new byte[1024];

和一个大Buffer,装客户端的所有数据,其中用到了strReceiveLength,是客户端发送的总长度,稍后再解释:

byte[] buffer = new byte[Convert.ToInt32(strReceiveLength)];

 

改写while (true)循环,每次接受1K的数据,然后用Array.Copy方法,把bufferTemp中的数据复制给buffer:

num = connSocket.Receive(bufferTemp, SocketFlags.None);

ArrayUtil.ArrayCopy(bufferTemp, buffer, check, num);

check += num;

这个Array.Copy是重点,因为TCP数据流在传输过程中也是一个包一个包的传送,最大不超过8K。所以每次接受到的数据,也就是bufferTemp这个变量有可能装满,也有可能装不满。所以在拷贝的时候一定按照这次接受的长度顺序的放入buffer中。等到客户端全部数据发送完成后,再把buffer转换:

strReceive = Encoding.UTF8.GetString(buffer, 0, buffer.Length);

而不能够每次都转换,再strReceive += 一个Byte数组。这样做的后果就是中文会被截断,因为中文在UTF-8编码下占3-4个字节,很容易出现乱码。

 

2)数据长度校验

TCP在传输过程中难免会有数据发送不全或者丢失的情况。所以在客户端发送数据的时候一定带上校验长度:

byte[] btyLength = Encoding.UTF8.GetBytes(strContent);

string strLength = btyLength.Length.ToString().PadLeft(8, '0');

string sendData = strLength + strContent;

byte[] buffer = Encoding.UTF8.GetBytes(sendData);

socketClient.Send(buffer);

这样在服务器端,先把要接受的长度收到:

byte[] bufferLength = new byte[8];
num = connSocket.Receive(bufferLength);

strReceiveLength = Encoding.UTF8.GetString(bufferLength, 0, bufferLength.Length);

在循环里用下面的判断,来校验和判断是否已经接受完毕:

if (check == Convert.ToInt32(strReceiveLength))

 

3)设计上一些方式

很多局域网的部署是分层的,也就是分内网和外网。服务器部署一定要在外网上部署,这里的外网指的是在客户端之上的网段上。

比如192.168.1.22下有个无线路由,无线连接的IP段为192.168.2.1~254

服务器搭建在192.168.1网段下,192.168.2的客户端是可以访问的。但是相反则不行,192.168.1网段下的设备无法主动找到192.168.2的服务器。

 

posted @ 2016-09-26 14:32  何鸿涛  阅读(10891)  评论(0编辑  收藏  举报