1.Socket相关概念
~socket叫做“套接字”,用于描述ip地址和端口。是一个通信链的句柄。
~socket非常类似于电话插座
~在Internet上有很多这样的主机,这些主机一般运行了很多个服务软件,同时提供几种服务.每种服务都打开一个socket,并绑定到一个端口上,不同的端口对应于不同的服务(应用程序).
~例如:http使用80端口 ftp使用21端口 smtp使用23端口
两种类型
一.流式Socket(STREAM):是一种面向连接的socket,针对于面向连接的TCP服务应用,安全,但是效率低
二.数据报式Socket(DATAGRAM):是一种无连接的Socket,对应于无连接的UDP服务应用,不安全(丢失,顺序错乱,在接收端要分析重排及要求重发),但效率高
2.Socket一般应用模式(服务器和客户端)
监听客户端请求,客户端, 连接套接字
服务端的Socket(至少两个)
一个负责接收客户端连接请求
每成功接收到一个客户端的连接便在服务端产生一个套接字
~为接收客户端连接时创建
~每一个客户端对应一个Socket
客户端Socket
客户端Socket
必须制定连接服务端地址和端口
通过创建一个Socket对象来初始化一个到服务器端的TCP连接
3.Socket通信基本流程
服务端:
1申请一个Socket
2绑定到一个ip地址和一个端口
3开启监听,等待接收连接
客户端
1申请一个socket
2连接服务器(指明ip地址和端口号)
!服务端接到连接请求后,产生一个新的socket(端口大于1024)与客户端建立连接并进行通讯,原监听socket继续听
4.服务器端
创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据)
获得文本框中的ip地址对象
创建包含ip和port的网络节点对象
namespace 聊天程序
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
TextBox.CheckForIllegalCrossThreadCalls
= false; //关闭对文本框的跨线程操作
}
Thread threadWatch
= null ;//负责监听的
线程
Socket socketWatch
= null ;//负责监听的
套接字
private void btnListen_Click(object sender, EventArgs e)
{
//
创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据)
socketWatch = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType .Tcp);
//获得文本框中的ip地址对象
IPAddress address
= IPAddress .Parse(txtbxIP.Text.Trim());
//创建包含ip和port的网络节点对象
IPEndPoint endPoint
= new IPEndPoint(address, int.Parse(txtbxPort.Text.Trim()));
//
负责监听的套接字 绑定到唯一的IP和端口上
socketWatch.Bind(endPoint);
//设置监听队列的长度,同时能够处理的连接最大数量,连接总数是没有上限的,取决于服务器的能力
socketWatch.Listen(10);
threadWatch = new Thread (WatchConnecting);
threadWatch.IsBackground = true; //设置为后台线程
threadWatch.Start(); //开启线程
ShowMsg( "服务器启动监听!" );
}
/// <summary>
/// 监听客户端请求的方法
/// </summary>
void WatchConnecting()
{
while (true )//持续不断的监听新的客户端的连接请求
{
//开启监听
客户端 连接请求 , 注意: Accept方法 会阻断当前的线程!
Socket sokConnection
= socketWatch.Accept();//一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection
ShowMsg( "客户端连接成功!" );
}
}
void ShowMsg(string msg)
{
txtbxMsg.AppendText(msg + "\r\n");
}
}
}
5.客户端
private void btnLink_Click(object sender, EventArgs e)
{
//得到IPAddress
IPAddress address
= IPAddress .Parse(txtbxIP.Text.Trim());
//得到IPEndPoint
IPEndPoint endPoint
= new IPEndPoint(address, int.Parse(txtbxPort.Text.Trim()));
//创建套接字
Socket socketClint
= new Socket( AddressFamily.InterNetwork, SocketType .Stream, ProtocolType.Tcp);
//套接字连接
socketClint.Connect(endPoint);
}
6.案例基础实现
TCP下的stream操作,一个服务器端对应多个客户端,实现服务器端选择性发送信息,客户端发送信息到服务端:
----------------------------------------------------服务端--------------------------------------------
namespace 聊天程序服务端 { public partial class Form1 : Form { public Form1() { InitializeComponent(); TextBox .CheckForIllegalCrossThreadCalls = false; //关闭对文本框的跨线程操作 } Thread threadWatch = null ; //负责监听的 线程 Socket socketWatch = null ; //负责监听的 套接字 Socket sokConnection = null ; // private void btnListen_Click( object sender, EventArgs e) { // 创建 服务端 负责监听 套接字 参数(使用ip4寻址地址,使用流式连接,使用tcp协议传输数据) socketWatch = new Socket ( AddressFamily.InterNetwork, SocketType .Stream, ProtocolType .Tcp); //获得文本框中的ip地址对象 IPAddress address = IPAddress .Parse(txtbxIP.Text.Trim()); //创建包含ip和port的网络节点对象 IPEndPoint endPoint = new IPEndPoint(address, int .Parse(txtbxPort.Text.Trim())); // 负责监听的套接字 绑定到唯一的IP和端口上 socketWatch.Bind(endPoint); //设置监听队列的长度,同时能够处理的连接最大数量,连接总数是没有上限的,取决于服务器的能力 socketWatch.Listen(10); threadWatch = new Thread (WatchConnecting); threadWatch.IsBackground = true ; //设置为后台线程 threadWatch.Start(); //开启线程 ShowMsg( "服务器启动监听!" ); } //保存了服务端所有负责和客户端通信的套接字 Dictionary <string , Socket> dict = new Dictionary < string, Socket >(); //保存服务器所有连接的通信套接字 receive方法的线程 Dictionary <string , Thread> dictThread = new Dictionary < string, Thread >(); /// <summary> /// 监听客户端请求的方法 /// </summary> void WatchConnecting() { while (true ) //持续不断的监听新的客户端的连接请求 { //开启监听 客户端 连接请求 , 注意: Accept方法 会阻断当前的线程! sokConnection = socketWatch.Accept(); //一旦监听到客户端的请求,就返回一个负责和该客户端通信的套接字sokConnection //向列表控件中 添加一个客户端的ip端口字符串,作为客户端的唯一标示 listbx.Items.Add(sokConnection.RemoteEndPoint.ToString()); //将于客户端通信的套接字对象 sokConnection 添加到 键值对集合中 并以客户端ip端口作为键 dict.Add(sokConnection.RemoteEndPoint.ToString(), sokConnection); ShowMsg( "客户端连接成功!" +sokConnection.RemoteEndPoint.ToString()); //创建 通信线程 ParameterizedThreadStart pts = new ParameterizedThreadStart(RecMsg); Thread tdRec = new Thread(pts); tdRec.IsBackground = true ;//设置为后台线程 tdRec.SetApartmentState( ApartmentState .STA); tdRec.Start(sokConnection); dictThread.Add(sokConnection.RemoteEndPoint.ToString(), tdRec); } } /// <summary> /// 接收字符串 /// </summary> void RecMsg(object sok) { Socket sokConn = sok as Socket; byte [] recByte = new byte[1024 * 1024 * 2]; while (true ) { int length = -1; try { length = sokConn.Receive(recByte); //接收二进制数据字节流 } catch (SocketException se) { ShowMsg( "异常: " + se.Message + sokConn.RemoteEndPoint.ToString()); //从通信套接字字典中移除被中断连接的 通信套接字对象 dict.Remove(sokConn.RemoteEndPoint.ToString()); //从 通信线程 集合中 删除 被中断连接的 通信线程 dictThread.Remove(sokConn.RemoteEndPoint.ToString()); //从列表中删除 被中断连接的 ip:Port listbx.Items.Remove(sokConn.RemoteEndPoint.ToString()); break ; } catch (Exception ex) { ShowMsg( "异常: " + ex.Message); break ; } //将接收到的流转换成string类型 if (recByte[0] == 0)//判断 发送过来的数据 的第一个元素0 发送过来的是文本 { //此时 是将 数组 所有元素 都转换成字符串 而真正接受到 只有几个服务端的几个字节 string strMagRec = System.Text.Encoding .UTF8.GetString(recByte, 1, length-1); ShowMsg(strMagRec); } else if (recByte[0] == 1) { SaveFileDialog sfd = new SaveFileDialog(); if (sfd.ShowDialog() == System.Windows.Forms. DialogResult.OK) { string fileSavePath = sfd.FileName; //获得保存文件的路径 //创建文件流 然后让文件流来根据路径创建一个文件 using (FileStream fs = new FileStream (fileSavePath, FileMode .Create)) { fs.Write(recByte, 1, length - 1); ShowMsg( "文件保存成功:" + fileSavePath); } } } } } void ShowMsg(string msg) { txtbxMsg.AppendText(msg + "\r\n" ); } /// <summary> /// 发送实现 /// </summary> private void btnSend_Click( object sender, EventArgs e) { //必须选择发送对象 if (string .IsNullOrEmpty(listbx.Text)) { MessageBox .Show("请选择要发送的对象" ); } else { string strMsg = txtbxMsgSnd.Text.Trim(); if (string .IsNullOrEmpty(strMsg)) { ShowMsg( "发送文本不能为空!请输入!" ); return ; } //将要发送的字符串转成uft8对应的 数组 byte [] arrMag = System.Text.Encoding .UTF8.GetBytes(strMsg); byte [] arrMagSend = new byte[arrMag.Length+1]; Buffer .BlockCopy(arrMag, 0, arrMagSend, 1, arrMag.Length); string strClintKey = listbx.SelectedItem.ToString(); try { dict[strClintKey].Send(arrMagSend); } catch (SocketException se) { MessageBox .Show("异常: " + se.Message); return ; } ShowMsg( "发送出去:" + strMsg); //sokConnection.Send(arrMag); } } /// <summary> /// 群发 /// </summary> private void btnSendAll_Click( object sender, EventArgs e) { string strMsg = txtbxMsgSnd.Text; byte [] arrMsg = System.Text.Encoding .UTF8.GetBytes(strMsg); byte [] arrMagSend = new byte[arrMsg.Length + 1]; Buffer .BlockCopy(arrMsg, 0, arrMagSend, 1, arrMsg.Length); foreach (Socket sok in dict.Values) { try { sok.Send(arrMagSend); } catch (SocketException se) { MessageBox .Show("异常: " + se.Message); return ; } } ShowMsg( "群发完毕:)" ); } } }
----------------------------------客户端----------------------------------------------
namespace 聊天程序客户端 { public partial class Form1 : Form { public Form1() { InitializeComponent(); TextBox .CheckForIllegalCrossThreadCalls = false; } Thread threadRec = null ; //创建接收线程 Socket socketClint = null ; //创建接收套接字 /// <summary> /// 连接服务器 /// </summary> private void btnLink_Click( object sender, EventArgs e) { //得到IPAddress IPAddress address = IPAddress .Parse(txtbxIP.Text.Trim()); //得到IPEndPoint IPEndPoint endPoint = new IPEndPoint(address, int .Parse(txtbxPort.Text.Trim())); //创建客户端套接字 socketClint = new Socket ( AddressFamily.InterNetwork, SocketType .Stream, ProtocolType .Tcp); //套接字连接 socketClint.Connect(endPoint); ShowMsg( "连接上服务器了!!哦也!" ); threadRec = new Thread (RecMsg); threadRec.IsBackground = true ; threadRec.Start(); } /// <summary> /// 接收方法 /// </summary> void RecMsg() { //定义一个 接收用的 缓存区(2M字节数组) byte [] recByte = new byte[1024 * 1024 * 2]; while (true ) { int length = -1; try { length = socketClint.Receive(recByte); //接收二进制数据字节流 } catch (SocketException se) { //Exception是最顶级的异常父类,我们最好用最接近的异常,这里用SocketException ShowMsg( "异常: " + se.Message); break ; } catch (Exception ex) { //里面涉及装箱操作,会多一些操作 ShowMsg( "异常:" + ex.Message); } //将接收到的流转换成string类型 if (recByte[0] == 0)//判断 发送过来的数据 的第一个元素0 发送过来的是文本 { //此时 是将 数组 所有元素 都转换成字符串 而真正接受到 只有几个服务端的几个字节 string strMagRec = System.Text.Encoding .UTF8.GetString(recByte, 1, length - 1); ShowMsg(strMagRec); } else if (recByte[0] == 1) { SaveFileDialog sfd = new SaveFileDialog(); if (sfd.ShowDialog() == System.Windows.Forms. DialogResult.OK) { string fileSavePath = sfd.FileName; //获得保存文件的路径 //创建文件流 然后让文件流来根据路径创建一个文件 using (FileStream fs = new FileStream (fileSavePath, FileMode .Create)) { fs.Write(recByte, 1, length - 1); ShowMsg( "文件保存成功:" + fileSavePath); } } } } } #region 在窗体文本框中显示字符串 - ShowMsg(string msg) /// <summary> /// 在窗体文本框中显示字符串 /// </summary> /// <param name="msg"></param> void ShowMsg(string msg) { txtbxMsg.AppendText(msg + "\r\n" ); } #endregion #region 发送文本-btnSend_Click /// <summary> /// 发送文本 /// </summary> private void btnSend_Click( object sender, EventArgs e) { string strMsg = txtbxSndMsg.Text.Trim(); //将要发送的字符串转成uft8对应的 数组 byte [] arrMag = System.Text.Encoding .UTF8.GetBytes(strMsg); byte [] arrMagSend = new byte[arrMag.Length + 1]; Buffer .BlockCopy(arrMag, 0, arrMagSend, 1, arrMag.Length); //拷贝 try { socketClint.Send(arrMagSend); //发送 } catch (SocketException se) { MessageBox .Show("异常: " + se.Message); return ; } ShowMsg( "发送出去:" + strMsg); //sokConnection.Send(arrMag); } #endregion #region 选择要发送的文件 - void btnOpenFile_Click /// <summary> /// 选择发送文件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnOpenFile_Click( object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog(); if (ofd.ShowDialog() == System.Windows.Forms. DialogResult.OK) { txtbxFilePath.Text = ofd.FileName; } } #endregion /// <summary> /// 向服务器发送文件 /// </summary> private void btnSendFile_Click( object sender, EventArgs e) { //用文件流打开用户选择的文件 //使用using是因为FileStream是挺占内存的类,使用using是为了更好地释放内存 using (FileStream fs = new FileStream (txtbxFilePath.Text,FileMode .Open)) { byte [] arrMsg = new byte[1024 * 1024 * 2]; //2M大小 //将文件数据读到 数组 int length = fs.Read(arrMsg, 0, arrMsg.Length); byte [] arrFileSend = new byte[length + 1]; arrFileSend[0] = 1; //代表发送的是文件数据 //块拷贝 将arrMsg数组的元素从0开始拷贝,拷贝到arrFileSend从1开始,拷贝length长度 Buffer .BlockCopy(arrMsg, 0, arrFileSend, 1, length); //流拷贝 缺点 从0开始拷贝 //arrMsg.CopyTo(arrFileSend, 0); try { socketClint.Send(arrFileSend); } catch (SocketException se) { MessageBox .Show("异常: " + se.Message); return ; } ShowMsg( "已发送:" + txtbxFilePath.Text); } } } }