1. Socket通信
在Windows.Networking.Sockets命名空间下提供了支持Socket通信相关的类型。有趣的是,这些类型的命名中并没有出现如TCP、UDP等关键词,官方似乎有意避开这些传统的命名方式,而是按照各通信协议的功能来命名。可参考如下:
- DatagramSocket ——用UDP协议的Socket网络通信
- StreamSocket —— 通过流方式接收/发送网络数据,实际上是基于TCP协议的Socket通信。在服务器端,可以使用StreamSocketListener来监听连接请求
- MessageWebSocket与StreamWebSocket —— 使用WebSocket相关技术进行网络通信
(1) 基于UDP协议的通信
DatagramSocket类封装了基于UDP数据报相关的网络通信功能。由于UDP协议是无连接协议,资源消耗较少,处理效率高,经常被用于传输要求不太严格的数据,如聊天信息、网络视频等。
在服务器端,DatagramSocket类的使用步骤如下:
- 创建DatagramSocket实例。
- 处理MessageReceived事件,当收到新的消息时会引发该事件。
- 调用BindEndpointAsync方法绑定本地终结点,包括地址和端口;如果希望在本地任何地址上都监听到数据,可以调用BindServiceNameAsync方法,该方法仅仅绑定本地端口。
而在客户端,直接实例化一个DatagramSocket对象后,就可以直接通过GetOutputStreamAsync方法获取一个输出流对象以向远程主机发送数据(直接将数据写入该流即可),在调用时应指定远程主机的地址(IP地址或主机名)和接收端口。
接下来通过一个示例来演示UDP协议通信的应用方法。本示例既可以作为监听服务器,也可以作为客户端使用,因此可以在同一台机器上进行测试。本示例实现客户端将文本消息发送到服务器,服务器将显示收到的文本消息。
在页面布局中,可以使用Pivot控件来添加两个选项卡,一个用于服务器,另一个用于客户端。
<Pivot>
<PivotItem Header="服务器">
<Grid RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="在客户端输入以下IP地址:" FontSize="20"/>
<TextBlock x:Name="tbIp" FontSize="36" IsTextSelectionEnabled="True"/>
</StackPanel>
<ListView x:Name="lvMsg" Grid.Row="1">
<ListView.Header>
<TextBlock Foreground="LightPink" Text="收到的消息列表" FontSize="20"/>
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="3,12">
<TextBlock FontSize="20" Foreground="Yellow">
来自
<Run Text="{Binding Path=FromIP}"/>
的消息:
</TextBlock>
<TextBlock TextWrapping="Wrap" FontSize="24" Text="{Binding Path=Message}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</PivotItem>
<PivotItem Header="客户端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServer" Header="服务器IP: "/>
<TextBox x:Name="txtMessage" Header="消息内容: " TextWrapping="Wrap" Height="160"/>
<Button HorizontalAlignment="Center" Content="发送" Tapped="Button_Tapped"/>
</StackPanel>
</PivotItem>
</Pivot>
为了在测试应用程序时能够轻松得知服务器的IP地址,可以获取本地网络的显示名称,并显示在界面上。在页面重写的OnNavigatedTo方法中加入以下代码:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//显示服务器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
//...
}
GetHostNames方法会返回多个主机名,随后通过FirstOrDefault扩展方法将设备中的以太网卡连接筛选出来。h.Type == HostNameType.Ipv4表示只选出v4版本的IP地址;NetworkAdapter.IanaInterfaceType属性类型是一个整数值,此处数值6表示以太网卡,如果希望使用本地无线网卡进行通信,可以使用数值71进行筛选。
服务器所使用的监听端口号将通过一个常量值来固定:
/// <summary>
/// 用于接收数据的端口
/// </summary>
const string SERVICE_PORT = "795";
在类级别中声明必要的变量,主要是两个用于通信的Socket对象,一个用于服务器,另一个用于客户端。
/// <summary>
/// 用于服务器的Socket
/// </summary>
DatagramSocket svrSocket = null;
/// <summary>
/// 用于客户端的Socket
/// </summary>
在重写的OnNavigatedTo方法中添加服务器监听实现:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//显示服务器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
svrSocket = new DatagramSocket();
// 添加接收消息事件处理
svrSocket.MessageReceived += SerSocket_Received;
// 绑定到本地端口
await svrSocket.BindServiceNameAsync(SERVICE_PORT);
clientSocket = new DatagramSocket();
}
处理DatagramSocket对象的MessageReceived事件,显示收到的消息。
async void SerSocket_Received(DatagramSocket sender,DatagramSocketMessageReceivedEventArgs args)
{
// 获取相关的DataReader对象
DataReader reader = args.GetDataReader();
// 读取消息内容
uint len = reader.UnconsumedBufferLength;
string msg = reader.ReadString(len);
// 远程主机
string remoteHost = args.RemoteAddress.DisplayName;
reader.Dispose();
// 显示接收到的消息
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
()=>
{
lvMsg.Items.Add(new {
FromIP = remoteHost, Message = msg});
});
}
下面代码实现客户端向服务器发送消息:
private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
{
// 获取输出流
using (var outStream = await clientSocket.GetOutputStreamAsync(new HostName(txtServer.Text),SERVICE_PORT))
{
using (var writer = new DataWriter(outStream))
{
// 写入流
writer.WriteString(txtMessage.Text);
//提交写入的内容
await writer.StoreAsync();
}
}
}
在获取到输出流后,可以使用DataWriter类来向流对象写入数据,使用该类所封装的WriteString方法可以直接写入字符串内容。默认情况下是使用UTF-8编码格式,一般不需要修改。
要注意的是,在写完数据后,要调用StoreAsync方法,被写入的数据才会提交到输出流中。
示例运行效果:UDP
使用UDP的另一篇博客,可参考:添加链接描述
(2) 通过TCP协议传输数据
与UDP不同,TCP协议是基于连接的,即在通信之前,客户端需要连接服务器。这意味着TCP对数据的次序与完整性要求更为严格,确保数据准确无误地达到目标终端。例如,文件传输就应当使用TCP协议来完成,因为少一个字节或者多一个字节都有可能损坏文件。
StreamSocket类封装了基于TCP协议的Socket通信功能,在客户端,通常遵循以下顺序来使用StreamSocket类:
- 实例化StreamSocket对象。
- 调用ConnectAsync方法连接服务器。
- 通过OutputStream属性返回的输出流就可以发送数据;而InputStream属性则返回输入流对象,用于接收数据。
- 当不再使用Socket时,调用Dispose方法释放其占用的相关资源。
在服务器中,则需要一个StreamSocketListener实例,绑定本地主机或某个端口,以监听客户端的连接请求。当有客户端发出连接请求时,会引发ConnectionReceived事件,从事件参数中可以获取一个与客户端进行通信的StreamSocket实例。
下面示例将演示如何使用基于TCP协议的Socket通信。
本例将服务器与客户端合并在一个应用程序中,应用程序既可以充当服务器角色,也可以用作客户端。在客户端选择一个图像文件并输入一些文本,应用程序会将图像与文本一起发送到服务器。随即服务器会显示收到的数据。
页面布局XAML如下:
<Pivot>
<PivotItem Header="服务器">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Spacing="10">
<TextBlock Text="服务器IP地址:" Height="30"/>
<TextBlock x:Name="tbSvIP" FontSize="24" IsTextSelectionEnabled="True" Height="30"/>
</StackPanel>
<ListBox x:Name="lbItems" Grid.Row="1">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Width="50" Height="50" Stretch="UniformToFill" Source="{Binding Path=Image}"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap" FontSize="18" Text="{Binding Path=Text}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</PivotItem>
<PivotItem Header="客户端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServerIp" Header="服务器地址:"/>
<Button Content="选择图像...