Socket Basic Concepts
Socket Basic Concepts
首先介绍Socket的一些基本概念
Socket是操作系统提供的一系列网络编程接口。
网络模型分若干层,也有一些协议,比如TCP协议,UDP协议等,这些都是抽象的定义,在硬件以及操作系统级别上有一些对应的实现,Socket可以看做操作系统为开发人员提供的一系列网络编程接口,它封装了一些协议的细节,比如怎么组织数据包,怎么发送数据之类的。
Socket编程的几个基本概念
Endpoint
Endpoin指定要连接到哪里,Endpoint包括两部分内容,IP和Port,IP地址和端口组合起来才能唯一指定远程的通信端。
AddressFamily
怎么寻址,有了IP地址之后就是如何寻址的问题,常用的寻址方案是IP V4和IP V6两种类型,windows操作系统从VISTA和Windows 20008起默认支持IPV6。
Protocol
使用什么协议进行通信,比如TCP协议或者UDP协议,下面介绍Socket类型的时候还会涉及TCP和UDP等协议的介绍。
Socket类型
Socket有三种常用类型:Stream, Dgram, Raw
Stream流类型,支持可靠、双向、基于连接的字节流,使用TCP协议。
Dgram数据报类型,支持数据报,即最大长度固定的无连接、不可靠消息。消息可能会丢失或重复并可能在到达时不按顺序排列,使用UDP协议。
Raw类型支持对基础传输协议的访问,需要自己生成数据包。网上有一些RAW的例子,比如D.O.S***,ARP***,网络监控之类的。
本文只讨论Stream类型的Socket编程,RAW和Dgram不在讨论之列,也就是只讨论基于TCP协议的编程。
一些常见的概念问题
Socket和TCP/IP有什么关系?
Socket和TCP/IP不是一个层面的概念,Socket是操作系统提供的操作TCP数据的编程接口。
Sockets V4、Sockets V5有什么区别?
经常看到一些软件可以设置Sockets4/Sockets5代理,简单说他们是客户端与外网服务器之间通讯的协议,Sockets是位于应用层与传输层之间的中间层。 Sockets V4支持TCP, Sockets V5支持TCP/UDP,支持安全认证,支持IPV6。
Socket能够同时接受和发送数据吗?
TCP协议是双工的
Socket如何保证数据按顺序到达?
TCP协议来保证
Socket的基本通信模型模型
客户端:
Socket()
Connect
Send
Close
服务器端:
Socket()
Bind
Listen
Accept
Receive
Send
Close
客户端和服务器端模型是不一样的,两边是非对称的。
.Net Socket API
下面是.Net Socket编程最基本的几个类,位于命名空间System.Net.Sockets
Socket Socket接口类
TcpClient TCP客户端类
TcpListener TCP侦听类
NetworkStream 用于网络访问的基础数据流
其他经常用到的辅助类,位于命名空间System.Net
Dns 域名解析
EndPoint 标识网络地址
IPAddress IP地址。
NetworkCredential 基于密码的身份验证方案,不支持基于公钥的身份验证方法(比如ssl)
一个Socket的简单例子
输入网址,获得HTML页面的一段演示代码,只是演示Socket对象的几个主要功能,不具有实用价值。
基本步骤为:建立Socket对象,连接服务器,发送数据,然后接受数据,对应上一章介绍的Socket通信模型。
代码
01 | < span style = "font-size: 10pt;" >private string DownloadPage(string path) |
03 | Uri uri = new Uri(path); |
04 | Encoding encoding = Encoding.UTF8;// .GetEncoding("gb2312"); |
06 | string requestHeader = BuildRequestHeader(uri); |
07 | byte[] requestBytes = encoding.GetBytes(requestHeader); |
08 | byte[] receivedBytes = new byte[1024 * 100]; |
10 | Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); |
11 | socket.Connect(uri.Host,uri.Port); |
12 | socket.Send(requestBytes); |
14 | int receivedBytesLength = socket.Receive(receivedBytes); |
15 | socket.Shutdown(SocketShutdown.Both); |
18 | string html = string.Empty; |
19 | if (receivedBytesLength > 0) |
21 | html = encoding.GetString(receivedBytes, 0, receivedBytesLength); |
构造HTTP Header的代码如下,注意不要忘了Http头模板的最后一行
01 | < span style = "font-size: 10pt;" >private string BuildRequestHeader(Uri uri) |
03 | string httpHeaderTemplate = @"GET {url} HTTP/1.1 |
08 | return httpHeaderTemplate.Replace("{url}", uri.AbsolutePath) |
09 | .Replace("{host}", uri.Host); |
30秒思考题:这段简单代码有什么问题?
我们定义的用来接收数据的数组大小是固定的,如果要接受的数据超过数组大小怎么办? 可以定义一个缓冲区,每次接受固定大小的数据,直到接收完成为止。示例代码如下:
01 | < span style = "font-size: 10pt;" >MemoryStream ms = new MemoryStream(); |
04 | Console.WriteLine("Available :{0}", socket.Available); |
05 | int receivedBytesLength = socket.Receive(receivedBytes, 0, receivedBytes.Length, SocketFlags.None); |
06 | if (receivedBytesLength > 0) |
08 | ms.Write(receivedBytes, 0, receivedBytesLength); |
16 | string html = string.Empty; |
19 | html = encoding.GetString(ms.ToArray(), 0, (int)ms.Length); |
Socket的缓冲区
Socket接收数据时,操作系统先把数据接收到缓冲区,然后通知程序,socket.Available 是从已经从网络接收的、可供读取的数据的字节数,这个值是指缓冲区中已接收数据的字节数,不是实际的数据大小。而且如果网络有延迟,Send之后马上读取Available属性不一定能读到正确的值,所以不能利用socket.Available来判断总共要接受的字节数。
在上面的方法中,如果没有可读取的数据,则 Receive 方法将一直处于阻止状态,直到有数据可用,如果Server端也没有正确关闭连接,程序很容易死在这里,可以通过Socket.ReceiveTimeout来设置Socket对象接受数据的超时时间。
30秒思考题:为什么这样下载的页面有时候和浏览器下载的页面不一样?
>>gzip,chunked编码,重定向等
NetworkStream的例子
前面讲过基于TCP协议的Socket是Steam类型的,在操作系统中,为了简化编程,把设备、文件等都看作流对象,统一编程接口。NetworkStream类提供了在阻止模式下通过Socket套接字发送和接收数据的方法,.Net还提供了TcpClient和TcpListener类,用于简化同步阻止模式下通过TCP协议连接、发送和接收流数据。下面的例子是这几个对象的简单介绍,省略了一些细节,也不具有实用价值。
这个例子模拟计算机远程控制,先新起一个线程模拟服务进程,在这个线程中创建一个TcpListener对象,等待客户端连接。用户在客户端界面点了“连接”按钮后,UI线程创建TcpClient对象,等待用户输入dos命令,用户输入dos命令,按执行按钮,这时TcpClient对象把用户输入的命令发送给TcpListener对象,服务进程执行完命令后,将执行结果反馈给TcpClient对象。
部分代码。
01 | < span style = "font-size: 10pt;" >private void StartServer() |
03 | TcpListener server = new TcpListener(IPAddress.Any, 10000); |
05 | Debug.WriteLine("Server: start"); |
07 | TcpClient client = server.AcceptTcpClient(); |
08 | Debug.WriteLine("Server : connection accept"); |
09 | NetworkStream stream = client.GetStream(); |
11 | Process process = new Process(); |
12 | process.StartInfo.FileName = "cmd.exe"; |
13 | process.StartInfo.UseShellExecute = false; |
14 | process.StartInfo.RedirectStandardInput = true; |
15 | process.StartInfo.RedirectStandardOutput = true; |
16 | process.StartInfo.RedirectStandardError = true; |
17 | process.StartInfo.CreateNoWindow = false; |
20 | process.OutputDataReceived += (Object sender, DataReceivedEventArgs e) => |
22 | byte[] bytes = Encoding.GetEncoding("gb2312").GetBytes(e.Data + "\r\n"); |
23 | stream.Write(bytes, 0, bytes.Length); |
25 | process.BeginOutputReadLine(); |
29 | Byte[] buffer = new Byte[1024 * 10]; |
30 | int length = stream.Read(buffer, 0, buffer.Length); |
33 | Debug.WriteLine("Server: read 0 byte"); |
37 | string command = Encoding.GetEncoding("gb2312").GetString(buffer, 0, length); |
38 | Debug.WriteLine("Server: receive {0} ", command); |
40 | StreamWriter Writer = process.StandardInput; |
41 | Writer.WriteLine(command); |
46 | process.WaitForExit(1000); |
48 | Debug.WriteLine("Server: close"); |
52 | private void ConnectButton_Click(object sender, EventArgs e) |
54 | client = new TcpClient(); |
55 | client.Connect("localhost", 10000); |
56 | Console.WriteLine("Client: connect"); |
57 | StartButton.Enabled = true; |
60 | private void StartButton_Click(object sender, EventArgs e) |
62 | if (CommandTextBox.Text.Trim().Length == 0) |
67 | string request = CommandTextBox.Text.Trim(); |
68 | byte[] bytes = Encoding.GetEncoding("gb2312").GetBytes(request); |
69 | NetworkStream stream = client.GetStream(); |
70 | stream.Write(bytes, 0, bytes.Length); |
72 | Console.WriteLine("Client: request {0}", request); |
73 | MessageTextBox.AppendText("\r\nresponse from server:\r\n"); |
75 | byte[] buffer = new byte[1024]; |
77 | int receivedBytesLength = stream.Read(buffer, 0, buffer.Length); |
78 | if(receivedBytesLength > 0) |
80 | string text = Encoding.GetEncoding("gb2312").GetString(buffer, 0, receivedBytesLength); |
81 | MessageTextBox.AppendText(text); |
82 | MessageTextBox.AppendText("\r\n"); |
88 | }while(stream.DataAvailable); |