1、简介
本实例将使用目前常用的开发工具C#来开发服务器端,以CANoe端作为客户端。服务器端和客户端,通过TCP/IP连接,实现数据交换。
首先在服务器端建立一个监听Socket,自动创建一个监听线程,随时监听是否有客户端的连接。它只起到监听作用,当监听线程监听到客户端请求时,监听线程就调用Socket上的消息响应函数OnAccept,接收客户端的连接请求。服务器为每一个客户端请求建立一个Socket,以便并行建立消息响应,服务器端为了接收数据,必须为客户端建立消息响应函数OnReceive,用于接收数据。客户端为了接收服务器端的数据,需要在连接的Socket上建立一个消息响应函数OnReceive,用来接收数据。
2、C# TCP/IP服务器端开发
在Visual Studio 开发环境中利用C#开发一个TCP/IP服务器端程序。
2.1、新建工程
在Visual Studio 开发环境中,单击"文件"→"新建"→"项目"进入"新建项目"对话框。在Visual C#下面选择模板"Windows窗体应用程序"新建一个工程,并将项目命名为TCP_Demo,如图所示。
2.2、界面设计
2.2.1、控件属性列表
2.2.2、最终界面
2.3、C#代码实现
由于程序需要使用多线程监听客户端是否接入,以及接收客户端的数据,本程序除了要添加System.NET.Sockets外,还要需要添加System.Threading。以下为C#完整代码。
cs
using System;
using System.Windows.Forms;
using System.Net;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.ComponentModel;
namespace TCP_Demo
{
public partial class Form1 : Form
{
//定义一个TcpClient类
static TcpClient client = null;
//定义一个NetworkStream类, 用于网络访问的基础流的数据操作
static NetworkStream stream = null;
//定义一个TcpServer类
TcpListener server = null;
//线程用于实时监测client是否接入,以及是否数据传过来
public Thread SocketWatch;
delegate void SetTextCallback(string text);
//0 - 断开状态; 1- 尝试连接中; 2- 已连接;3- 尝试断开中;
public int socket_status = 0;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
//获取本地计算机的主机名
string name = Dns.GetHostName();
//获取本地计算机的Internet协议(IP)地址
IPAddress[] ipadrlist = Dns.GetHostAddresses(name);
foreach (IPAddress ipa in ipadrlist)
{
//检索IPv4的IP地址
if (ipa.AddressFamily == AddressFamily.InterNetwork)
{
lstShow.Items.Add("检索到主机的IP地址:" + ipa.ToString());
//将第一个有效的IPv4地址作为服务器地址
if (txtServerIP.Text == "")
txtServerIP.Text = ipa.ToString();
}
}
btnSend.Enabled = false;
btnEnd.Enabled = false;
}
private void btnStart_Click(object sender, EventArgs e)
{
btnStart.Enabled = false;
int port = Convert.ToInt32(txtServerPort.Text);
IPAddress IP = System.Net.IPAddress.Parse(txtServerIP.Text);
IPEndPoint p = new IPEndPoint(IP, port);
server = new TcpListener(p);
server.Start();
lstShow.Items.Add("服务器已启动!");
lstShow.Items.Add("等待客户端连接....");
Update();
socket_status = 1; //等待client接入
//开启新的线程,用于监控client接入和数据接收
this.SocketWatch = new Thread(new ThreadStart(this.SocketTask));
this.SocketWatch.Start();
btnSend.Enabled = true;
btnEnd.Enabled = true;
}
private void btnSend_Click(object sender, EventArgs e)
{
byte[] sendmsg = new byte[512]; //定义发送的Byte数组
int i = 0;
//Encode转换为UTF8
byte[] msg = Encoding.UTF8.GetBytes(txtSend.Text);
for (i = 0; i < msg.Length;i++ )
{
sendmsg[i] = msg[i];
}
//CANoe根据最后字符是否为'\0'来判断接收的字符串长度
sendmsg[msg.Length] = 0x00;
stream = client.GetStream();
stream.Write(sendmsg, 0, sendmsg.Length);
lstShow.Items.Add("发送数据成功!");
}
private void btnEnd_Click(object sender, EventArgs e)
{
if (this.SocketWatch != null) this.SocketWatch.Abort();
if (stream != null) stream.Close();
if (client != null) client.Close();
if (server != null) server.Stop();
socket_status = 0;
btnStart.Enabled = true;
btnSend.Enabled = false;
btnEnd.Enabled = false;
}
//用于实时监测client是否接入,以及是否数据传过来
public void SocketTask()
{
while (true)
{
if (socket_status==1)
{
//接受挂起的连接请求。
client = server.AcceptTcpClient();
socket_status = 2;
}
Byte[] bytes = new Byte[256];
//返回用于发送和接收数据的NetworkStream
stream = client.GetStream();
try
{
stream.Read(bytes, 0, bytes.Length);//从读取Stream数据
//按照UTF8的编码方式得到字符串
string data = Encoding.UTF8.GetString(bytes, 0, bytes.Length);
if (bytes[0] != '\0') // 如果数据是有效的
{
this.SetText(data);//数据传递给主线程
}
}
catch (System.IO.IOException)
{
if (client.Connected == true)
{
client.Close(); //释放此TcpClient实例
stream.Close(); //关闭当前流并释放与之关联的所有资源
}
socket_status = 3;
break;
}
}
}
private void SetText(string text)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.txtReceive.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.txtReceive.Text = text;
lstShow.Items.Add("接收到数据:" + text);
}
}
private void timer1_Tick(object sender, EventArgs e)
{
//处理界面的更新
Socket s;
if (client != null)
{
if (socket_status ==3) //尝试断开中
{
server.Stop();
lstShow.Items.Add("客户端已断开!");
txtClientIP.Text = "";
txtClientPort.Text = "";
btnStart.Enabled = true;
btnSend.Enabled = false;
btnEnd.Enabled = false;
client = null;
socket_status = 0;
}
else
{
if (txtClientIP.Text == "")
{
lstShow.Items.Add("客户端已连接!");
s = client.Client;
//显示远程客户端IP和Port端口
txtClientIP.Text = s.RemoteEndPoint.ToString().Split(':')[0];
txtClientPort.Text = s.RemoteEndPoint.ToString().Split(':')[1];
}
}
}
}
}
}
3、CANoe TCP/IP客户端开发
对于CANoe客户端的演示,本实例力求简单,同时突出TCP/IP的通信功能,在实例设计中只使用一个网络节点和一个面板。面板主要用于演示与TCP/IP服务器端的数据发送与接收。当然,读者可以将这单一功能移植到自己的仿真工程中,将服务器端传过来的字符串信息根据项目需要与仿真工程的系统变量、环境变量、信号、报文等关联起来。服务器端也可以将CANoe发送字符进行解析。这种通过TCP/IP来远程监控CANoe运行的解决方案,可以广泛应用于日常的项目中。
3.1、创建仿真工程
在CANoe界面选择File→New,在可选的模板中选择CAN 500kBaud1ch(波特率为500kBaud,1通道)。在Simulation Setup窗口中添加一个网络节点为TcpClient,如图所示,并将其CAPL程序设定为TcpClient.can。
3.2、新建系统变量
为了与面板上的控件关联,方便数据的传递和处理,本实例需要创建表中所列出的系统变量。
3.3、Panel设计
3.3.1、Panel属性列表
与C#.NET程序的用户界面类似,在CANoe端也需要设计一个客户界面来与TCP/IP服务器端通信。下表为TCP/IP客户端面板的控件列表及属性设定,可以根据此列表来创建一个面板。
3.3.2、Panel最终界面
3.4、CAPL实现
TCP/IP的CAPL实现主要通过调用CANoe提供的相关CAPL函数。为了方便调用CAPL的Socket接口,Vector提供了一个IPCommon.can头文件。
3.4.1、头文件
cs
/*@!Encoding:1252*/
variables
{
const long INVALID_SOCKET = ~0;
const long WSA_IO_PENDING = 997;
const long WSAEWOULDBLOCK = 10035;
const dword INVALID_IP = 0xffffffff;
dword gIpAddress = INVALID_IP;
char gIpLastErrStr[1024] = "";
char gIpAddressStr[32] = "";
int gIpLastErr = 0;
dword gUdpPort = 0;
long gUdpSocket = INVALID_SOCKET;
char gUdpRxBuffer[4096];
dword gTcpPort = 0;
long gTcpSocket = INVALID_SOCKET;
long gTcpDataSocket = INVALID_SOCKET;
char gTcpRxBuffer[8192];
// status
int gStatus = 0;
const int gkSTATUS_UNINITIALISED = 0;
const int gkSTATUS_INITIALISED = 1;
}
long UdpRecv( dword socket)
{
int result = 0;
result = UdpReceiveFrom( socket, gUdpRxBuffer, elcount( gUdpRxBuffer));
if ( 0 != result)
{
gIpLastErr = IpGetLastSocketError( socket);
if ( WSA_IO_PENDING != gIpLastErr)
{
IpGetLastSocketErrorAsString( socket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "UdpReceive error (%d): %s", gIpLastErr, gIpLastErrStr);
}
}
return result;
}
long TcpRecv( dword socket)
{
int result = 0;
result = TcpReceive( socket, gTcpRxBuffer,elcount( gTcpRxBuffer));
if ( 0 != result)
{
gIpLastErr = IpGetLastSocketError( socket);
if ( WSA_IO_PENDING != gIpLastErr)
{
IpGetLastSocketErrorAsString( socket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "TcpReceive error (%d): %s", gIpLastErr, gIpLastErrStr);
}
}
return result;
}
3.4.2、主程序
在TcpClient.can中,CANoe作为客户端实现检索本机IP,并处理TCP/IP的连接、数据发送、数据接收及连接断开等操作。
cs
/*@!Encoding:1252*/
includes
{
#include "IPCommon.can"
}
variables
{
}
void SetupIp()
{
int adapterIndex = 1;
char text[512] = "";
char info[512] = "";
int size = 512;
long result = 0;
dword addresses[1];
writeClear(0);
if (1 > IpGetAdapterCount())
{
writelineex(0, 3, "Error: There is no network interface available!");
stop();
}
IpGetAdapterDescription(adapterIndex, text, size);
if (0 != IpGetAdapterAddress(adapterIndex, addresses, 1))
{
//try to check the 2nd interface
adapterIndex ++;
if (0 != IpGetAdapterAddress(adapterIndex, addresses, 1))
{
writelineex(0, 3, "Error: Could not retrieve ip address!");
stop();
}
}
gIpAddress = addresses[0]; // the interface used
if (INVALID_IP == gIpAddress)
{
writelineex(0, 3, "Error: ip address to be used is invalid!");
stop();
}
IpGetAdapterDescription(adapterIndex, text, size);
snprintf(info, size, "Interface: %s", text);
writelineex(0, 1, info);
IpGetAdapterAddressAsString(adapterIndex, text, size);
snprintf(info, size, "Ip address: %s", text);
writelineex(0, 1, info);
SysSetVariableString(sysvar::TCPIP::TcpClientIp, text);
IpGetAdapterMaskAsString(adapterIndex, text, size);
snprintf(info, size, "Subnet mask: %s", text);
writelineex(0, 1, info);
IpGetAdapterGatewayAsString(adapterIndex, text, size);
snprintf(info, size, "Gateway address: %s", text);
writelineex(0, 1, info);
@TCPIP::TcpClientPort=6566;
@TCPIP::TcpServerPort =6565;
gStatus = gkSTATUS_INITIALISED;
}
on start
{
SetupIp();
}
on stopMeasurement
{
ResetIp();
}
void OnTcpReceive( dword socket, long result, dword address, dword port, char buffer[], dword size)
{
char addressString[64] = "";
if ( gTcpDataSocket != socket)
{
writelineex(0, 2, "OnTcpReceive called for unknown socket 0x%X", socket);
return;
}
if (0 != result)
{
IpGetLastSocketErrorAsString( socket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "OnTcpReceive error (%d): %s", IpGetLastSocketError( socket), gIpLastErrStr);
return;
}
IpGetAddressAsString(address, addressString, elcount(addressString));
SysSetVariableString(sysvar::TCPIP::TcpData, buffer);
TcpRecv( socket);
}
void OnTcpSend( dword socket, long result, char buffer[], dword size)
{
if ( gTcpDataSocket != socket)
{
writelineex(0, 2, "OnTcpSend called for unknown socket 0x%X", socket);
}
if (0 != result)
{
IpGetLastSocketErrorAsString( socket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "OnTcpSend error (%d): %s", IpGetLastSocketError( socket), gIpLastErrStr);
}
}
void ConnectTcp()
{
char buffer[64];
dword serverIp;
SysGetVariableString(sysvar::TCPIP::TcpServerIp, buffer, elcount(buffer));
serverIp = IpGetAddressAsNumber(buffer);
if (INVALID_IP == serverIp)
{
writelineex(0, 1, "Error: invalid server Ip address!");
return;
}
gTcpPort = @sysvar::TCPIP::TcpClientPort;
gTcpDataSocket = TcpOpen(gIpAddress, gTcpPort);
if ( INVALID_SOCKET == gTcpDataSocket)
{
writelineex(0, 1, "Error: could not open Tcp socket!");
return;
}
else
{
writelineex(0, 1, "Tcp socket opened.");
}
if (0 == TcpConnect(gTcpDataSocket, serverIp, @sysvar::TCPIP::TcpServerPort))
{
writelineex(0, 1, "Successfully connected to server %s:%d", buffer, @sysvar::TCPIP::TcpServerPort);
TcpRecv( gTcpDataSocket);
}
}
void DisconnectTcp()
{
if (INVALID_SOCKET != gTcpDataSocket)
{
TcpClose(gTcpDataSocket);
gTcpDataSocket = INVALID_SOCKET;
}
writelineex(0, 1, "Tcp socket is closed.");
}
void SendTcpData()
{
char buffer[8192];
SysGetVariableString(sysvar::TCPIP::TcpClientData, buffer, elcount(buffer));
if (INVALID_SOCKET == gTcpDataSocket)
{
writelineex( 0, 2, "Tcp socket is invalid!");
return;
}
if (0 != TcpSend( gTcpDataSocket, buffer, elcount(buffer)))
{
gIpLastErr = IpGetLastSocketError( gTcpDataSocket);
if ( WSA_IO_PENDING != gIpLastErr)
{
IpGetLastSocketErrorAsString( gTcpDataSocket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "Tcp send error (%d): %s", gIpLastErr, gIpLastErrStr);
}
}
else
{
writelineex( 0, 1, "Tcp data sent successfully!");
}
}
void ResetIp()
{
if (INVALID_SOCKET != gTcpDataSocket)
{
TcpClose(gTcpDataSocket);
gTcpDataSocket = INVALID_SOCKET;
}
if (INVALID_SOCKET != gUdpSocket)
{
UdpClose(gUdpSocket);
gUdpSocket = INVALID_SOCKET;
}
}
void OnTcpConnect( dword socket, long result)
{
if ( gTcpDataSocket != socket)
{
writelineex(0, 2, "OnTcpConnect called for unknown socket 0x%X", socket);
return;
}
if (0 != result)
{
IpGetLastSocketErrorAsString( socket, gIpLastErrStr, elcount( gIpLastErrStr));
writelineex( 0, 2, "OnTcpConnect error (%d): %s", IpGetLastSocketError( socket), gIpLastErrStr);
return;
}
else
{
writelineex(0, 1, "Successfully connected to server via Tcp");
TcpRecv( socket);
}
}
on sysvar_update sysvar::TCPIP::TcpConnect
{
if (@this)
{
ConnectTcp();
}
}
on sysvar_update sysvar::TCPIP::TcpDisconnect
{
if (@this)
{
DisconnectTcp();
}
}
on sysvar_update sysvar::TCPIP::TcpSend
{
if (@this)
{
SendTcpData();
}
}
4、运行测试
先运行TCP/IP服务器端应用,服务器的IP地址为本机的IP地址,端口6565是程序的默认端口,也可以输入一个其他的端口号。单击"开始"按钮,服务器端应用将进入接入监听状态。
这时运行CANoe的Tcp_Client仿真工程,Client Panel的客户端IP地址也将自动设置为本机的IP地址,端口6566为默认端口,读者也可修改该端口号。在服务器IP地址栏和端口栏可以输入对应的服务器IP和端口。单击Connect to server按钮,服务器端将收到接入请求,这时服务器端将显示来自客户端的IP信息和端口号。成功连接以后,服务器端和客户端之间可以相互发送信息。
作为演示,可以允许服务器端应用和CANoe仿真工程运行在同一台计算机上,图为同一台计算机上演示TCP服务器端与客户端之间通信的效果图。
这里需要指出的是,为了演示方便,CANoe例程中采用自动识别IP地址,如果使用的计算机上有多个网络接口或蓝牙接口,建议在测试过程中将没有使用到的硬件禁掉,否则CANoe可能无法找到有效的IP地址。当然,也可以修改CANoe端的代码,去除自动识别本机IP地址的功能。