网络编程之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 {
4 //第一步:创建监听用的socket
5 Socket socket = new Socket
6 (
7 AddressFamily.InterNetwork, //使用ip4
8 SocketType.Stream,//流式Socket,基于TCP
9 ProtocolType.Tcp //tcp协议
10 );
12 //第二步:监听的ip地址和端口号
13 //ip地址
14 IPAddress ip = IPAddress.Parse(_ip);
15 //ip地址和端口号
16 IPEndPoint point = new IPEndPoint(ip, _point);
18 //绑定ip和端口
19 //端口号不能占用:否则:以一种访问权限不允许的方式做了一个访问套接字的尝试
20 //通常每个套接字地址(协议/网络地址/端口)只允许使用一次。
21 try
22 {
23 socket.Bind(point);
24 }
25 catch (Exception)
26 {
28 if (new IOException().InnerException is SocketException)
29 Console.WriteLine("端口被占用");
30 }
31 //socket.Bind(point);
33 //第三步:开始监听端口
35 //监听队列的长度
36 /*比如:同时有3个人来连接该服务器,因为socket同一个时间点。只能处理一个连接
37 * 所以其他的就要等待。当处理第一个。然后在处理第二个。以此类推
38 *
39 * 这里的10就是同一个时间点等待的队列长度为10,即。只能有10个人等待,当第11个的时候。是连接不上的
40 */
41 socket.Listen(10);
43 string msg = string.Format("服务器已经启动........\n监听ip为:{0}\n监听端口号为:{1}\n", _ip, _point);
44 showMsg(msg);
46 Thread listen = new Thread(Listen);
47 listen.IsBackground = true;
48 listen.Start(socket);
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;
10 //不停的接收来自客服端的连接
11 while (true)
12 {
13 //如果有客服端连接,则创建通信用是socket
14 //Accept方法。会阻塞线程。所以用多线程
15 //Accept方法会一直等待。直到有连接过来
16 Socket connSocket = socket.Accept();
18 //获取连接成功的客服端的ip地址和端口号
19 string msg = connSocket.RemoteEndPoint.ToString();
20 showMsg(msg + "连接");
22 //获取本机的ip地址和端口号
23 //connSocket.LocalEndPoint.ToString();
25 /*
26 如果不用多线程。则会一直执行ReceiveMsg
27 * 就不会接收客服端连接了
28 */
29 Thread th = new Thread(ReceiveMsg);
30 th.IsBackground = true;
31 th.Start(connSocket);
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 {
11 //接收数据
12 byte[] buffer = new byte[1024 * 1024];//1M
13 int num = 0;
14 try
15 {
16 //接收数据保存发送到buffer中
17 //num则为实际接收到的字节个数
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 }
38 //把实际有效的字节转化成字符串
39 string str = Encoding.UTF8.GetString(buffer, 0, num);
40 showMsg(connSocket.RemoteEndPoint + "说:\n" + str);
44 }
45 }
提供服务器的完整代码如下:
View Code
运行代码。显示如下
是不是迫不及待的想试试看效果。好吧其实我也跟你一样,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;
12 namespace WFAClient
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);
26 //端口号
27 IPEndPoint point = new IPEndPoint(ip, int.Parse(tbPoint.Text));
29 socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
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);
46 }
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 */