声明一下。这个系列是博客园一个高手的我转载来学了。不过对他的代码加了点小注释。嘿嘿
对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:
1. 什么是TCP/IP、UDP?
2. Socket在哪里呢?
3. Socket是什么呢?
4. 你会使用它们吗?
什么是TCP/IP、UDP?
TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。
UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
这里有一张图,表明了这些协议的关系。
图1
TCP/IP协议族包括运输层、网络层、链路层。现在你知道TCP/IP与UDP的关系了吧。
Socket在哪里呢?
在图1中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。
图2
原来Socket在这里。
Socket是什么呢?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
你会使用它们吗?
前人已经给我们做了好多的事了,网络间的通信也就简单了许多,但毕竟还是有挺多工作要做的。以前听到Socket编程,觉得它是比较高深的编程知识,但是只要弄清Socket编程的工作原理,神秘的面纱也就揭开了。
一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理,也许TCP/IP协议族就是诞生于生活中,这也不一定。
图3
先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
在这里我就举个简单的例子,我们走的是TCP协议这条路(见图2)。例子用MFC编写,运行的界面如下:
(Microsoft Foundation Classes),是一个微软公司提供的类库(class libraries),以C++类的形式封装了Windows的API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量。其中包含的类包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类。
图4
图5
在客户端输入服务器端的IP地址和发送的数据,然后按发送按钮,服务器端接收到数据,然后回应客户端。客户端读取回应的数据,显示在界面上。
客户端就一个函数完成了一次通信。在这里IP地址为何用127.0.0.1呢?使用这个IP地址,服务器端和客户端就能运行在同一台机器上,这样调试方便多了。当然你可以在你朋友的机器上运行Server程序(本人在局域网中测试过),在自己的机器上运行Client程序,当然输入的IP地址就该是你朋友机器的IP地址了。
简单的理论和实践都说了,现在Socket编程不神秘了吧?希望对你有些帮助。
附C#简单测试源码:/Files/lmjob/socket通信.rar
这个代码的解析:
服务端
int portNum = Convert.ToInt32(this.textBox1.Text);
//得到端口号码
TcpListener listener = new TcpListener(portNum);
//建立TCP监听端口
// TcpListener 类提供一些简单方法,用于在阻止同步模式下侦听和接受传入连接请求。可使用 TcpClient 或 Socket 来连接 TcpListener。可使用 IPEndPoint、本地 IP 地址及端口号或者仅使用端口号,来创建 TcpListener。可以将本地 IP 地址指定为 Any,将本地端口号指定为 0(如果希望基础服务提供程序为您分配这些值)。如果您选择这样做,可在连接套接字后使用 LocalEndpoint 属性来标识已指定的信息。
Start 方法用来开始侦听传入的连接请求。Start 将对传入连接进行排队,直至您调用 Stop 方法或它已经完成 MaxConnections 排队为止。可使用 AcceptSocket 或 AcceptTcpClient 从传入连接请求队列提取连接。这两种方法将阻止。如果要避免阻止,可首先使用 Pending 方法来确定队列中是否有可用的连接请求。
listener.Start();
this.richTextBox1.AppendText("Waiting for connection..." + "\r");
TcpClient client = listener.AcceptTcpClient();
//得到客户端的请求连接
//为使 TcpClient 连接并交换数据,使用 TCP ProtocolType 创建的 TcpListener 或 Socket 必须侦听是否有传入的连接请求。可以使用下面两种方法之一连接到该侦听器: · 创建一个 TcpClient,并调用三个可用的 Connect 方法之一。 · 使用远程主机的主机名和端口号创建 TcpClient。此构造函数将自动尝试一个连接。
this.richTextBox1.AppendText("Connection accepted." + "\r");
//System.Net.Sockets.tc
NetworkStream ns = client.GetStream();
//返回用于发送和接收数据的 NetworkStream。
//提供用于网络访问的基础数据流。 NetworkStream 类提供在阻止模式下通过 Stream 套接字发送和接收数据的方法。 若要创建 NetworkStream,必须提供连接的 Socket。 将 Write 和 Read 方法用于简单的单线程同步阻止 I/O。若要使用不同的线程来处理 I/O,则请考虑使用 BeginWrite 和 EndWrite 方法,或 BeginRead 和 EndRead 方法进行通信。 NetworkStream 不支持对网络数据流的随机访问
byte[] byteTime = Encoding.UTF8.GetBytes(this.textBox2.Text);
//得到要发送给客户端的数据。转换成BYTE类型数据
try
{
ns.Write(byteTime, 0, byteTime.Length);
// buffer
类型:array<System..::.Byte>[]()[] 类型 Byte 的数组,该数组包含要写入 NetworkStream 的数据。 offset 类型:System..::.Int32 buffer 中开始写入数据的位置。 size 类型:System..::.Int32 要写入 NetworkStream 的字节数。
ns.Close();
//传输完毕,就立即关闭NS
//这时等待客户端关闭client,客户端关闭完了,服务端才关闭
client.Close();
//只传输一次,就关闭了。对应的客户端的代码也是这样连接的。
// Close 方法将该实例标记为已释放并关闭 TCP 连接。调用此方法将关闭 Socket,并且还将关闭用于发送和接收数据的关联 NetworkStream(如果创建有一个这样的流)。
}
catch (Exception ee)
{
MessageBox.Show(ee.ToString());
}
listener.Stop();
//关闭监听端口
//
Stop 方法不会关闭任何已接受的连接。需要用户负责分别关闭这些连接。 |
}
客户端
private void button1_Click(object sender, EventArgs e)
{
try
{
int portNum = Convert.ToInt32(this.textBox2.Text);
//服务器端口,可以随意修改
string hostName = this.textBox1.Text; //服务器地址,127.0.0.1指本机
this.richTextBox1.AppendText("请求连接" + hostName + ":" + portNum.ToString() + "\r");
TcpClient client = new TcpClient(hostName, portNum);
//创建 TCP对象 TcpClient 构造函数 (String, Int32) 初始化 TcpClient 类的新实例并连接到指定主机上的指定端口。
hostname 类型:System..::.String 要连接到的远程主机的 DNS 名。 port 类型:System..::.Int32 要连接到的远程主机的端口号。
NetworkStream ns = client.GetStream();
// client.GetStream();
// 返回用于发送和接收数据的 NetworkStream。
byte[] bytes = new byte[1024]; //长度
int bytesRead = ns.Read(bytes, 0, bytes.Length);//返回字节长度
// buffer 类型:array<System..::.Byte>[]()[] 类型 Byte 的数组,它是内存中用于存储从 NetworkStream 读取的数据的位置。 offset 类型:System..::.Int32 buffer 中开始将数据存储到的位置。 size 类型:System..::.Int32 要从 NetworkStream 中读取的字节数。 返回值 类型:System..::.Int32 从 NetworkStream 中读取的字节数。
this.richTextBox1.AppendText(Encoding.UTF8.GetString(bytes, 0, bytesRead) + "\r");
//定义在1024个字符范围内去读取数据,然后得到真实数据长度。读出传输字节长度范围内所有数据。
//Rdstrm = new StreamReader(邮件服务流对象, System.Text.Encoding.GetEncoding("gb2312"));
//服务端当它收到TCP请求连接时就把数据NS直接写出,写完了就关闭NS。
client.Close();
//读完数据后,直接关闭TCP连接。
//服务端受到连接关闭后,也关闭它的连接
}
catch (Exception ee)
{
MessageBox.Show(ee.ToString());
this.richTextBox1.AppendText(ee.ToString() + "\r");
}
}