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的网络节点对象
TextBox
.CheckForIllegalCrossThreadCalls =
false
;
//关闭对文本框的跨线程操作
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);
}
}
}
}