客户端要连接服务器:首先要知道服务器的IP地址。而服务器里有很多的应用程序,每一个应用程序对应一个端口号 所以客户端想要与服务器中的某个应用程序进行通信就必须要知道那个应用程序的所在服务器的IP地址,及应用程序所对应的端口号
TCP协议:安全稳定,一般不会发生数据丢失,但是效率低。利用TCP发生数据一般经过3次握手(所有效率低,自己百度三次握手) UDP协议:快速,效率高,但是不稳定,容易发生数据丢失(没有经过三次握手,不管服务器有空没空,信息全往服务器发,所有效率搞,但服务器忙的时候就没办法处理你的数据,容易造成数据丢失,不稳定)
首先创建一个解决方案,在解决方案下创建一个“Socket通信”windows窗体应用程序的的项目,用作服务端,然后再在解决方案下创建一个“Socket客户端”windows窗体应用程序的项目 用作客户端
Socket通信 (服务器端)
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- namespace Socket通信
- {
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- this.txtPort.Text = "50000";
- this.txtIp.Text = "192.168.253.2";
- }
- private void btnStart_Click(object sender, EventArgs e)
- {
- try
- {
- //当点击开始监听的时候,在服务器端创建一个负责监听IP地址跟端口号的Socket
- Socket socketWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- //Any:提供一个 IP 地址,指示服务器应侦听所有网络接口上的客户端活动。此字段为只读。
- IPAddress ip = IPAddress.Any;
- //IPAddress ip = IPAddress.Parse(this.txtIp.Text);
- //创建端口号对象;将txtPort.Text控件的值设为服务端的端口号
- IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(txtPort.Text));
- //监听
- socketWatch.Bind(point);
- ShowMsg("监听成功"); //注意:这个ShowMeg方法是自己定义的。看下面的代码可以找到这个方法
- socketWatch.Listen(10);//连接队列的最大长度 ;即:一个时间点内最大能让几个客户端连接进来,超过长度就进行排队
- //等待客户端连接;Accept()这个方法能接收客户端的连接,并为新连接创建一个负责通信的Socket
- Thread th = new Thread(Listen); //被线程执行的方法如果有参数的话,参数必须是object类型
- Control.CheckForIllegalCrossThreadCalls = false; //因为.net不允许跨线程访问的,所以这里取消跨线程的检查。.net不检查是否有跨线程访问了,所有就不会报: “从不是创建控件“txtLog”的线程访问它” 这个错误了,从而实现了跨线程访问
- th.IsBackground = true; //将th这个线程设为后台线程。
- //Start(object parameter); parameter:一个对象,包含线程执行的方法要使用的数据,即线程执行Listen方法,Listen的参数
- th.Start(socketWatch); //这个括号里的参数其实是Listen()方法的参数。因为Thread th = new Thread(Listen)这个括号里只能写方法名(函数名) 但是Listen()方法是有参数的,所有就要用Start()方法将它的参数添加进来
- }
- catch
- { }
- }
- //将远程连接的客户端的IP地址和Socket存入集合中
- Dictionary<string, Socket> dic = new Dictionary<string, Socket>();
- /// <summary>
- /// 等待客户端连接,如果监控到有客户端连接进来就创建一个与之通信的Socket
- /// </summary>
- /// <param name="o"></param>
- Socket socketSend; // 定义一个负责通信的Socket
- void Listen(object o) //这里为什么不直接传递Socket类型的参数呢? 原因是:被线程执行的方法如果有参数的话,参数必须是object类型
- {
- Socket socketWatch = o as Socket;
- while (true) //为什么这里要有个while循环?因为当一个人连接进来的时候创建了与之通信的Socket后就程序就会往下执行了,就不会再回来为第二个人的连接创建负责通信的Socket了。(应该是每个人(每个客户端)创建一个与之通信的Socket)所以要写在循环里。
- {
- try
- {
- //等待客户端连接;Accept()这个方法能接收客户端的连接,并为新连接创建一个负责通信的Socket
- socketSend = socketWatch.Accept();
- dic.Add(socketSend.RemoteEndPoint.ToString(), socketSend); //(根据客户端的IP地址和端口号找负责通信的Socket,每个客户端对应一个负责通信的Socket),ip地址及端口号作为键,将负责通信的Socket作为值填充到dic键值对中。
- //我们通过负责通信的这个socketSend对象的一个RemoteEndPoint属性,能够拿到远程连过来的客户端的Ip地址跟端口号
- ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + "连接成功");//效果:192.168.1.32:连接成功
- comboBox1.Items.Add(socketSend.RemoteEndPoint.ToString()); //将连接过来的每个客户端都添加到combBox控件中。
- //客户端连接成功后,服务器应该接收客户端发来的消息。
- Thread getdata = new Thread(GetData);
- getdata.IsBackground = true;
- getdata.Start(socketSend);
- }
- catch
- { }
- }
- }
- /// <summary>
- /// 服务端不停的接收客户端发送过来的消息
- /// </summary>
- /// <param name="o"></param>
- void GetData(object o)
- {
- Socket socketSend = o as Socket;
- while (true)
- {
- try
- {
- //将客户端发过来的数据先放到一个字节数组里面去
- byte[] buffer = new byte[1024 * 1024 * 2]; //创建一个字节数组,字节数组的长度为2M
- //实际接收到的有效字节数; (利用Receive方法接收客户端传过来的数据,然后把数据保存到buffer字节数组中,返回一个接收到的数据的长度)
- int r = socketSend.Receive(buffer);
- if (r == 0) //如果接收到的有效字节数为0 说明客户端已经关闭了。这时候就跳出循环了。
- {
- //只有客户端给用户发消息,不可能是发0个长度的字节。即便发空消息,空消息也是有过个长度的。所有接收到的有效字节长度为0就代表客户端已经关闭了
- break;
- }
- //将buffer这个字节数组里面的数据按照UTF8的编码,解码成我们能够读懂的的string类型,因为buffer这个数组的实际存储数据的长度是r个 ,所以从索引为0的字节开始解码,总共解码r个字节长度。
- string str = Encoding.UTF8.GetString(buffer, 0, r);
- ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + str);
- }
- catch
- {
- }
- }
- }
- private void ShowMsg(string str)
- {
- txtLog.AppendText(str + "\r\n"); //将str这个字符串添加到txtLog这个文本框中。
- }
- /// <summary>
- /// 服务器给客户端发送消息
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void btnSend_Click(object sender, EventArgs e)
- {
- if (comboBox1.SelectedItem == null) //如果comboBox控件没有选中值。就提示用户选择客户端
- {
- MessageBox.Show("请选择客户端");
- return;
- }
- //服务器要想给客户端发消息,就需要先拿到负责通信的那个Socket
- string str = txtMes.Text.Trim();
- byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
- string getIp = comboBox1.SelectedItem as string; //comboBox存储的是客户端的(ip+端口号)
- socketSend = dic[getIp] as Socket; //根据这个(ip及端口号)去dic键值对中找对应 赋值与客户端通信的Socket【每个客户端都有一个负责与之通信的Socket】
- socketSend.Send(buffer);
- }
- }
- }
Socket客户端 (客户端)
- using System;
- using System.Collections.Generic;
- using System.ComponentModel;
- using System.Data;
- using System.Drawing;
- using System.Linq;
- using System.Net;
- using System.Net.Sockets;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using System.Windows.Forms;
- namespace Socket客户端
- {
- public partial class Form1 : Form
- {
- public Form1()
- {
- InitializeComponent();
- this.txtIp.Text = "192.168.253.2"; //因为我们要在客户端连接服务端,所以这里是服务端的IP
- this.txtPort.Text = "50000"; //因为我们要在客户端连接服务端,所以这里服务端的端口号
- }
- Socket socketSend;
- private void btnStart_Click(object sender, EventArgs e)
- {
- try
- {
- //创建负责通信的Socket
- socketSend = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- IPAddress ip = IPAddress.Parse(this.txtIp.Text);
- //创建一个IPEndPoint类的实例,用远程服务器的IP地址和端口号来初始化它
- IPEndPoint point = new IPEndPoint(ip, Convert.ToInt32(this.txtPort.Text));
- //获得要连接的远程服务器应用程序的IP地址和端口号,并建立与远程服务器的连接
- socketSend.Connect(point);
- ShowMsg("连接成功");
- //开启一个新的线程,不停的接收服务端发送过来的消息
- Thread th = new Thread(GetData);
- th.IsBackground = true; //IsBackground获取或设置一个值,该值指示某个线程是否为后台线程。
- th.Start();
- }
- catch
- {
- }
- }
- void ShowMsg(string str)
- {
- this.txtLog.AppendText("\r\n"+str + "\r\n");
- }
- /// <summary>
- /// 客户端不停的接收服务端发送过来的消息
- /// </summary>
- /// <param name="o"></param>
- void GetData(object o)
- {
- while (true)
- {
- try
- {
- //将服务端发过来的数据先放到一个字节数组里面去
- byte[] buffer = new byte[1024 * 1024 * 2]; //创建一个字节数组,字节数组的长度为2M
- //实际接收到的有效字节数; (利用Receive方法接收客户端传过来的数据,然后把数据保存到buffer字节数组中,返回一个接收到的数据的长度)
- int r = socketSend.Receive(buffer);
- if (r == 0) //如果接收到的有效字节数为0 说明客户端已经关闭了。这时候就跳出循环了。
- {
- //只有客户端给用户发消息,不可能是发0个长度的字节。即便发空消息,空消息也是有过个长度的。所有接收到的有效字节长度为0就代表客户端已经关闭了
- break;
- }
- //将buffer这个字节数组里面的数据按照UTF8的编码,解码成我们能够读懂的的string类型,因为buffer这个数组的实际存储数据的长度是r个 ,所以从索引为0的字节开始解码,总共解码r个字节长度。
- string str = Encoding.UTF8.GetString(buffer, 0, r);
- //RemoteEndPoint方法是获取服务器的IP地址和端口号
- ShowMsg(socketSend.RemoteEndPoint.ToString() + ":" + str);
- }
- catch
- {
- }
- }
- }
- /// <summary>
- /// 客户端给服务器发送消息
- /// </summary>
- /// <param name="sender"></param>
- /// <param name="e"></param>
- private void btnSend_Click(object sender, EventArgs e)
- {
- //获取输入框输入的数据
- string str = txtMes.Text.Trim();
- //将输入框的数据转换成二进制数据
- byte[] buffer = System.Text.Encoding.UTF8.GetBytes(str);
- //Send方法是将数据发送到连接的Socket
- socketSend.Send(buffer);
- this.txtLog.AppendText("我自己:" + this.txtMes.Text);
- this.txtMes.Text = "";
- }
- private void Form1_Load(object sender, EventArgs e)
- {
- //在程序加载的时候取消跨线程的检查
- Control.CheckForIllegalCrossThreadCalls = false;
- }
- }
- }
当这两个项目(服务端,和客户端)都写好后,怎么测试呢?
首先我们将服务端设置启动项,然后启动调试,
然后我们在将鼠标移动到“Socket客户端” (客户端)这个项目下,鼠标右键项目名称“Socket客户端”--》调试--》启动实例 就可以了。
开打开始命令 cmd telnet 10.18.16.46 5000 即telnet 服务器ip地址 绑定的端口号
如果用命令,需要在 控制面板--》程序和功能--》打开或关闭windows功能 将Telnet 服务器,和Telnet客户端打上钩
---------------------------------------------------------------------------------------------------------------
注释一下:
//创建一个用来监听的Socket对象(参数1:表示采用IPv4,参数2:表示使用数据流来传输数据,而不是数据包 参数3:表示采用Tcp协议)
Socket skConn =newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
//创建IP和监听端口(参数1:IP地址 参数2:端口号是9999)
IPEndPoint endPoint=newIPEndPoint(IPAddress.Parse("192.168.253.3"),9999);