1. Socket技术
Socket通信介绍
Socket痛信介绍
同步通信与异步通信
通信技术
通信技术介绍
通信技术介绍
视频链接
异步通信
1.1 Socket技术本质
Socket被称为“套接字”,抽象化为类,程序设计者只需要创建Socket类对象,即可使用套接字。简单的说就像一个插座,连接应用层与运输层,并不需要理解是如何实现的,相当于在TCP/IP协议基础上封装起来,客户端与服务端通过插座作为中介用网线连接起来。在使用时我们只需要知道IP地址与端口号。
1.2 Socket技术如何使用
基本思想:基于文件类的操作,现在要往文件内写入或者读取文件中的内容,最主要的三个操作”打开-读写-关闭“。Socket就是该模式的一个实现,把它当作一种特殊的文件,而Socket类中的函数就可以对其进行操作(读、写IO、打开、关闭)
2.TCP协议
socket是一套用于不同主机之间通信的API,工作在TCP/IP 协议栈之上,通过socket与不同主机之间建立通信,我们只需要指定主机的IP地址和一个端口号,IP地址是唯一标识你的网络设备,如果没有端口,操作系统中则没有办法区分数据到底应该发送到哪一个应用上,因此端口主要用于区分主机上的不同应用,通过socket我们可以建立不同应用之间的虚拟通道,并且是点对点的
一个形象的比喻是将一条数据线连接在不同应用的插槽上,这也是socket这个名字的由来,我么经常用到的socket又两种类型:TCP、UDP,以下主要介绍TCP
TCP主要有2个特点:TCP协议是非常可靠的,它底层会自动检测并回传跌势的数据包,因此,对于调用者来说,你发送的数据对方一定会接收到,其次,发送和接收到的数据顺序是完全一致的,比如,你发送了一串字符,
对方就一定会元丰不懂的收到同样的字符串,这也是为什么大家说TCP是基于“数据流”的协议。
另外,需要注意的是,TCP需要收发数据的双方扮演不同的角色:服务器和客户端,服务器会被动等待客户端的连接,它自己不会主动发起请求,
3.同步通信与异步通信
同步通信:发送一个数据包以后,一直等到接收方响应,才可以发送下一个数据包(串行通信)
实例:客户端与服务端一对一连接,且在相邻时间内,单向通信。
异步通信:客户端请求之后,不必等到服务器回应之后就可以发送下一条请求。(并行运行 ),即在同步的基础上增加线程。
实例:一个服务端,多个客户端,多个客户端同一时间向服务端发送消息;一个客户端,一个服务端,客户端不同功能同一时间发送多条消息。如果此时用同步通信,服务端一下子收到多条数据,数据之间的字符串会乱码。
4.TCP程序设计
socket程序主要包括服务端、客户端,以下分别就服务端与客户端程序开发步骤做简要概述。
4.1 服务端程序开发步骤
服务端程序开发步骤主要分为以下几步:
- 创建Socket套接字
- IP地址和端口号与服务端绑定
- 创建监听序列
- Send与Recieve操作
- 关闭通信
(1)创建Socket套接字:这一步几乎是固定模式
AddressFamily.InterNetwork:IP地址寻址方式
SocketType.Stream:套接字类型
ProtocolType.Tcp:TCP协议
Socket server=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
(2)IP地址和端口号与服务端绑定:这一步几乎是固定模式
引入参数IPEndPoint:网络节点表示为IP地址与端口号
IPEndPoint point = new IPEndPoint(IPAddress.Parse(“IP地址”,int.Parse("端口号"));
服务端.Bind(point);
IP地址作为服务端的唯一标识符,表明身份,端口号作为连接的通道,通过Bind方式绑定
(3)创建监听序列:listen() 或accept()
server.Listen(3); //最多只能与3个客户端连接
Socket Client =server.Accept(); //从缓存中一个一个抓取,抓取的连接请求是客户端的,故定义为Client
如果不用Accept,Listen(3)相当于连接最大个数,当为4个,则超过限制,如果用Accept,相当于所有连接请求放在动态数组中,从另一个地方拿出来,但是一次只拿一个,此时Listen(3)、Listen(1)、Listen(10)都没有关系
(4) Send与Recieve操作:程序的核心部分,要实现的具体操作
Recieve接收部分:
byte[] b = new byte[1024 * 1024 * 2]; //数据接收缓存区,2M
int length = 0;
length = Client.Receive(b); //使用try/catch防止连接过程中断线
string msg=Encoding.Default.GetString(b,0,length); //字节解码成字符串
Send接收部分:
客户端.Send(Encoding.Default.GetBytes(“消息字符串”);
(5) 关闭通信
服务端.Shutdown(SocketShutdown.Both);
服务端.Close();//关闭Socket连接
在socket关闭之前,要确保已经发送和接收完所有挂起的数据,因此在关闭socket之前,要先调用
服务端.Shutdown(SocketShutdown.Both);
如果设计到异步通信,同时应关闭异步通信中创建的线程。
注意事项:在服务端的程序设计中存在以下对应关系,避免程序设计出错
***服务端.listen();
Socket 客户端= 服务端.accept();
客户端.recieve();
客户端.send();***
4.2 客户端程序开发步骤
客户端程序编写步骤如下:
(1)创建套接字:与服务端相同
(2)Connect申请连接:这一步与服务端端口绑定类似
IPEndPoint endPoint = new IPEndPoint(服务端IP地址, 服务端开放的端口号); // 用指定的ip和端口号初始化IPEndPoint实例
客户端.Connect(endPoint); //与远程主机建立连接
(3)Send()与Recieved()进行通信:与服务端相同
(4)关闭通信:与服务端相同
4.3 服务端案例
4.3.1 界面布局
4.3.2 服务端代码
完整程序代码:代码中有详细注释
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace 通信实例
{
public partial class Server : Form
{
public Server()
{
InitializeComponent();
}
/// <summary>
/// 应用程序主入口点
/// </summary>
static void main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Server());
}
//创建套接字
Socket server=new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//创建键和值的集合,string类型的变量和Socket
private Dictionary<string,Socket> ClientList = new Dictionary<string, Socket>();
/// <summary>
/// 启动服务器
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
IPEndPoint point = new IPEndPoint(IPAddress.Parse(textBox1.Text),int.Parse(textBox2.Text));
try
{
server.Bind(point); //每个套接字协议(地址/端口号)只允许使用一次,多次使用会报错,
//相当于一台电脑、一个串口,二者都想做服务器,
//电脑启用做服务器后,串口就无法再启用充当服务器,
//即一个文件无法打开2次,如果可以,后面的操作以哪一个文件为准呢?
}
catch(Exception ex) //使用try/catch的理由,避免已经被占用
{
MessageBox.Show("无法启动服务器" + ex.Message);
}
server.Listen(3); //最多只能与3个客户端连接,Listen与Accept并存,以Accept为准
Task.Run(() => //如果不用Accept,Listen(3)相当于连接最大个数,当为4个,则超过限制
{ //如果用Accept,相当于所有连接请求放在动态数组中,从另一个地方拿出来,
//但是一次只拿一个,此时Listen(3)、Listen(1)、Listen(10)都没有关系
ListenSocket();
});
MessageBox.Show("服务器启动成功");
button1.Enabled = false; //服务器只需要启动一次
}
/// <summary>
/// 创建监听程序
/// </summary>
private void ListenSocket()
{
while(true)
{
Socket Client =server.Accept(); //从缓存中一个一个抓取,抓取的连接请求是客户端的,故定义为Client
string client =Client.RemoteEndPoint.ToString(); //告诉服务器是谁连接了,只读,读客户端身份
Log(client + "连接了服务器");
ClientList.Add(client, Client); //抓取的客户端地址和连接请求
OnlineClient(client, true); //需要知道哪些客户端连接了服务器,
//即当服务器向不同客户端发送不同消息时,得明确是往那个客户端发送的
Task.Run(() =>
{
ReceiveMsg(Client);
});
}
}
/// <summary>
/// 已连接客户端列表更新
/// </summary>
/// <param name="client"></param>
/// <param name="p"></param>
private void OnlineClient(string client,bool p)
{
if(comboBox1.InvokeRequired) //跨线程操作,OnlineClient-ListenSocket--Task.Run(() =>中
{
Invoke(new Action(() => //线程中创建新的线程,跨线程使用关键字Invoke
{
if (p)
{
comboBox1.Items.Add(client);
}
else
{
foreach (string item in comboBox1.Items)
if (item == client)
{
comboBox1.Items.Add(client);
break;
}
}
}));
}
else //非跨线程操作,线程后台执行时,2个线程名称相同
{
if (p)
{
comboBox1.Items.Add(client);
}
else
{
foreach (string item in comboBox1.Items)
if (item == client)
{
comboBox1.Items.Add(client);
break;
}
}
}
}
/// <summary>
/// 接收消息
/// </summary>
/// <param name="Client"></param>
private void ReceiveMsg(Socket Client )
{
while(true)
{
byte[] b = new byte[1024 * 1024 * 2]; //数据接收缓存区
int length = 0;
string client=Client.RemoteEndPoint.ToString(); //获取连接的客户端IP地址
try
{
length = Client.Receive(b); //使用try/catch防止连接过程中断线
}
catch
{
OnlineClient(client, false);
Log(client + ":失去连接");
ClientList.Remove(client);
break;
}
if(length> 0)
{
string msg=Encoding.Default.GetString(b,0,length); //字节解码成字符串
Log(client + ":" +msg);
}
else
{
OnlineClient(client, false);
Log(client + ":失去连接");
ClientList.Remove(client);
}
}
}
/// <summary>
/// 创建一个Listview接收消息的方法
/// </summary>
/// <param name="info"></param>
string time = DateTime.Now.ToString("yyyy-mm-dd HH:MM:SS");
private void Log(string info)
{
if (!listView1.InvokeRequired)
{
ListViewItem lit=new ListViewItem(time);
lit.SubItems.Add(info);
listView1.Items.Insert(listView1.Items.Count , lit);
}
else
{
Invoke(new Action(()=>
{
ListViewItem lit = new ListViewItem(time);
lit.SubItems.Add(info);
listView1.Items.Insert(listView1.Items.Count, lit);
}));
}
}
/// <summary>
/// 服务器发送消息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
if(comboBox1.SelectedItem!=null)
{
Log("" + richTextBox1.Text.Trim());
string client=comboBox1.SelectedItem.ToString();
ClientList[client].Send(Encoding.Default.GetBytes(richTextBox1.Text));
}
else
{
MessageBox.Show("请选择客户端");
}
}
/// <summary>
/// 群发消息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
if (comboBox1.Items.Count>0 )
{
Log("" + richTextBox1.Text.Trim());
//string client = comboBox1.SelectedItem.ToString();
foreach(string item in comboBox1.Items)
{
ClientList[item].Send(Encoding.Default.GetBytes(richTextBox1.Text));
}
}
else
{
MessageBox.Show("没有客户端连接到服务器");
}
}
/// <summary>
/// 启动客户端
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click(object sender, EventArgs e)
{
Form frm = new Client();
frm.Show();
}
}
}
4.2.3 连接示范
以串口调试助手作为客户端,多次开启,如下:
连接后的情况如图所示: