
简介Android应用源码围绕安卓与PC之间的Socket通信展开以C#编写PC端服务逻辑面向需要掌握跨平台网络通信的移动开发与桌面开发人员。资源包含完整的C#服务端工程与Android客户端工程并附有编译好的exe与apk安装包下载后可直接在Windows和安卓设备上运行体验从连接建立、消息发送到数据收发的完整联调流程适合学习TCP/Socket通信、局域网互联及双端数据交互的开发者参考。包内共78个文件覆盖cs、java等源码文件class、dex等编译产物xml、png等配置与界面资源以及sln、csproj等工程配置整体约1.72MB目录结构清晰便于按需查阅和二次开发。目前已有118人学习/下载借助现成项目可快速理解Socket通信的核心逻辑为后续自定义通信协议或功能扩展提供起点。1. 安卓与PC的Socket通信为什么要用C#写服务端很多做物联网或工控的人会遇到这样一个需求Android设备采集数据要实时发给PC上的程序做展示或处理。常见做法是HTTP或MQTT但局域网直连、低延迟场景直接用Socket更干脆。这个源码包正好提供了一个完整闭环C#写的PC端服务端MySocketServerAndroid客户端SocketTest2还带编译好的exe和apk。第一次看到我反而愣了一下服务端不用Java而用C#细想又完全在理——你做上位机本来就用WinForm/WPF的话C#的Socket编程和UI调度是一条链路没必要为一个小模块引入第二套生态。适合先用这份源码把通信链路跑通再去改业务逻辑的人也适合想完整看一次TCP Socket握手、收发、断线重建过程的学生或初级工程师。2. Android客户端与C#服务端的Socket通信原理与源码结构2.1 TCP Socket通信模型谁监听谁连接Socket网络编程最容易搞反的是角色定位。这个项目里C#服务端MySocketServer运行在PC上先创建TcpListener绑定端口并进入监听状态Android客户端SocketTest2主动发起connect。连接建立后双向数据流都通过同一个TcpClient对应的NetworkStream读写。所以“服务端”不是指功能更强大而是指先启动、固定IP和端口、等待别人连入的一方Android端永远扮演发起连接的角色。可以先用一段极简的C#骨架理解这个模型TcpListener listener new TcpListener(IPAddress.Any, 9999); // 监听所有网卡 listener.Start(); Console.WriteLine(waiting for android client...); TcpClient client await listener.AcceptTcpClientAsync(); // 等待连接 Console.WriteLine($client connected: {client.Client.RemoteEndPoint});这段代码做了三件事绑定IP和端口、启动监听、接受一个客户端连接。IPAddress.Any表示监听PC上所有网卡的9999端口这样无论手机连的是WiFi还是网线只要能与PC的某个IP互通就能连上。AcceptTcpClientAsync是异步等待阻塞当前线程直到有客户端进入。之后拿到的client对象就代表一条已经建立的TCP通道。服务端与客户端在参数和职责上有明显区别拆项目前先把这个表看清楚角色宿主关键操作网络行为C#服务端PCbind/listen/accept被动接受连接端口固定Android客户端手机/模拟器connect主动发起连接使用服务端IP服务端默认监听0.0.0.0也就是所有网卡最后连接时客户端填的IP地址必须能被Android设备访问到。常见错误是服务端只在127.0.0.1上监听那只能本机自己连自己手机永远超时。2.2 项目源码结构与关键类解压这个zip后能看到两个工程目录。我拆这类源码的经验是服务端看入口和会话管理客户端看界面层和网络层。MySocketServer是C#工程从命名看核心类是Main入口和连接处理逻辑SocketTest2是Android Studio工程一般会有一个MainActivity负责UI一个TcpClientHelper或者类似的类封装Socket操作。关键文件对应关系大致如下工程文件/类作用MySocketServerProgram.cs / Form1.cs程序入口启动TcpListener并循环acceptMySocketServerClientSession / HandleClient每个连接的读写循环SocketTest2MainActivity输入IP/端口/消息触发连接和发送SocketTest2TcpClientHelperSocket连接、输出流、输入流封装具体文件以解压后为准但套路是一样的。看到这里你应该能预计到服务端把accept到的TcpClient交给一个会话线程处理客户端把连接和UI拆开避免界面卡顿。这个项目之所以适合当学习源码就是因为它的类划分足够典型又不会像生产级框架那样包装太多层。2.3 为什么PC端选C#而不是Java或Python可能很多人默认Android项目配Java服务端但其实C#做上位机才是这个场景的最优解。第一你如果已经在用WinForm/WPF做PC软件加一个TcpListener只是几十行代码不用再引入Tomcat或Node进程第二C#的async/await让Socket的并发接受和读写逻辑非常直白相比Java的阻塞IO或者Python的GIL写起来更顺手第三这个Demo把服务端打包成了exe双击就能回复数据对于只想测Android代码的人省掉了搭建服务端环境的成本。当然C#服务端也有边界跨平台需要装.NET运行时如果部署到Linux工控机要么改用.NET Core发布要么还是选Java系。但作为局域网通信的上位机Windows C#已经是很多工厂设备软件的基础配置这也是这个源码在检索时会被大量C#上位机开发者反复看的原因。3. C#服务端与Android客户端的核心代码实现与参数调优3.1 服务端TcpListener监听与回显服务端实现的核心在于accept循环和每个连接的处理循环。下面这段代码是MySocketServer里最常见的主程序形态TcpListener listener new TcpListener(IPAddress.Any, 9999); listener.Start(); while (true) { TcpClient client await listener.AcceptTcpClientAsync(); _ HandleClientAsync(client); // 不等待继续接受下一个连接 } static async Task HandleClientAsync(TcpClient client) { using (NetworkStream stream client.GetStream()) { byte[] buffer new byte[4096]; while (true) { int read await stream.ReadAsync(buffer, 0, buffer.Length); if (read 0) break; // 客户端关闭读返回0 string msg Encoding.UTF8.GetString(buffer, 0, read); Console.WriteLine($recv: {msg}); string echo $echo: {msg}; byte[] send Encoding.UTF8.GetBytes(echo); await stream.WriteAsync(send, 0, send.Length); } } }循环结构是先进入等待连接的无限循环每个连接到来后立刻创建一个后台任务处理它主循环继续accept下一个。HandleClientAsync中ReadAsync一直阻塞读取客户端发来的字节流读到0表示对端关闭或网络异常。把读到的字节按UTF-8转成字符串打印再原样加一个“echo:”前缀发回去这是验证双向通信最直接的回显逻辑。参数说明buffer设为4096字节如果业务包超过这个长度后面的内容会留在系统缓冲区导致被拆到下一次ReadAsync。二进制协议建议把buffer设到8K或16K并配合长度字段处理文本协议可以先用4096跑通再根据最大报文调整。Encoding.UTF8同时用于解码和编码能避免中文乱码。3.2 Android客户端Socket连接与数据收发Android端不能用主线程做网络操作否则直接抛NetworkOnMainThreadException。所以我一般会建一个独立的TcpClientHelper把连接和读写都放在工作线程里。下面这段是Demo中SocketTest2最核心的连接和发送代码public class TcpClientHelper { private Socket socket; private DataOutputStream out; public void connect(String ip, int port) throws IOException { socket new Socket(); socket.connect(new InetSocketAddress(ip, port), 3000); // 3秒连接超时 socket.setSoTimeout(5000); // 读超时5秒 out new DataOutputStream(socket.getOutputStream()); } public void send(String text) throws IOException { out.write(text.getBytes(UTF-8)); out.flush(); } }先用空构造的Socket再通过connect传入地址和超时时间这是为了给连接操作加上超时限制避免手机在WiFi不可达时卡住十几秒。setSoTimeout控制的是读操作等待时间如果服务端一直不回包read()会在5秒后抛出SocketTimeoutException从而让UI线程有机会提示用户。发送数据前转成UTF-8字节流对端C#服务端才能按同一编码解析。调用时放在new Thread或RxJava的IO线程里连接成功后还要用Handler把收到的数据传回UI线程。如果直接在主线程new SocketAndroid Studio会直接标红这是C#开发者在转Android时最容易踩的坑——C#里async/await自动回同步上下文Java原生线程没有这个便利。3.3 关键参数对比与调优建议把两端涉及的参数放在一起能更清楚看到调优方向。下面表格是我跑通这个Demo后整理下来最实用的一组默认值参数位置参数名项目默认示例建议值C#服务端监听IPIPAddress.AnyAny避免只绑127.0.0.1C#服务端端口99991024之上避开已知服务端口C#服务端接收缓冲区4096最大报文2倍以上Android客户端连接超时3000ms同一局域网1500-3000msAndroid客户端读超时5000ms按服务端响应时间调整两端字符编码UTF-8统一UTF-8最容易被忽略的是编码统一。服务端C#里如果用默认的GBK客户端Java用UTF-8中文通信就会变成乱码。另一个常见问题是端口选在8080或8081这类开发端口容易和本机Web服务冲突表现就是服务端启动时报“bind: only one usage of each socket address”也就是热词里搜到的那条错误。后面一章讲到排错时第一个检查的就是这个。编码、超时、缓冲区这三组参数调好一个稳定的TCP文本通信闭环基本就成立了。如果你想传文件或大包还要在应用层加分包和重组但那是下一阶段的事了。4. 联调实战从connect失败到稳定收发数据的排错过程4.1 服务端启动失败端口被占用与bind报错先看服务端最典型的是启动时抛SocketExceptionbind: only one usage of each socket address。这个报错意思是端口被其他程序占用了或者上一个程序退出后连接还处在TIME_WAIT状态。在Windows上先用下面命令找出是谁占用了9999端口netstat -ano | findstr 9999 taskkill /PID 进程号 /Fnetstat的-a显示所有连接-n用数字地址-o显示进程PID。第一行通常能看到LISTENING状态带PID把它结束掉再重开服务端即可。如果是TIME_WAIT状态等几十秒或换一个端口更快。还有一种情况是第二次运行服务端时报同样的错因为上次启动的进程还在后台跑着。用netstat确认后再启动比盲目改端口更不容易给客户端添乱。4.2 Android客户端连接超时从权限到防火墙的排查表客户端connect不上的原因会更多我会按下面的顺序查检查项工具/命令预期结果同一局域网ping PC_IP能ping通INTERNET权限AndroidManifest.xml已声明uses-permission手机访问地址设置不能用127.0.0.1用PC的局域网IP模拟器访问网络配置模拟器用10.0.2.2代替局域网IPWindows防火墙New-NetFirewallRule放行TCP 9999入站Android应用没有INTERNET权限时connect会报Permission denied而不是超时这个错很有迷惑性。另外一个非常常见的问题是在Android Studio里用模拟器调试模拟器网络栈把10.0.2.2映射到了宿主PC的loopback所以模拟器里必须填10.0.2.2真机才填PC的局域网IP。把这两点混在一起时经常会花掉一晚上。防火墙如果是默认阻止入站需要在管理员PowerShell里执行New-NetFirewallRule -DisplayName AndroidSocket9999 -Direction Inbound -Protocol TCP -LocalPort 9999 -Action Allow这条命令创建一条入站规则只放开TCP 9999端口。执行后再从手机连接通常就可以通过了。防火墙排查完连接就能建立但数据收发还没有结束。另外一个排查点是Android Studio运行日志。connect失败会抛ConnectException或SocketTimeoutException在Logcat里过滤“SocketException”就可以看到具体阶段是Failed to connect还是Connection refused。Connection refused说明服务端没监听或防火墙回了RSTFailed to connect通常是IP不可达。4.3 回显正常但数据出现粘包/半包服务端读到“echo: helloecho: world”这样的连在一起的内容时先不要怀疑Socket坏了。TCP是字节流协议它不保存应用消息边界发送方连续write两次接收方可能一次read就全部拿到也可能只拿到一半。处理办法是在消息前加上固定长度包头比如前4字节存消息长度服务端先读满4字节再读对应长度的字节byte[] lenBuf new byte[4]; await stream.ReadAsync(lenBuf, 0, 4); int msgLen BitConverter.ToInt32(lenBuf, 0); byte[] msgBuf new byte[msgLen]; await stream.ReadAsync(msgBuf, 0, msgLen);这里用BitConverter把4字节转为int但是需要约定字节序。C#默认用系统小端Java端DataOutputStream writeInt默认也是大端所以两端直接互转int会拿到一个很大的数字或负数。更稳妥的做法是两端都手动指定BigEndianC#里把int转成字节数组再反转Android端坚持用DataInputStream的readInt因为它按大端解释。粘包问题在文本协议里也许可以容忍但要做成稳定框架必须在第一次联调结束时引入长度帧。5. 把Demo改成可用框架多客户端、心跳与断线重连5.1 多客户端管理与线程安全Demo中的服务端用一个异步任务处理一个连接但没保存连接句柄。要支持多台Android设备同时在线需要维护一个客户端列表。我一般用ConcurrentDictionaryConcurrentDictionarystring, TcpClient clients new(); string key client.Client.RemoteEndPoint.ToString(); clients.TryAdd(key, client);连接断开时记得TryRemove否则心跳检测会误判。多客户端同时发数据时每个TcpClient有独立的NetworkStream写入不会互相阻塞所以不需要再加锁但如果多个线程要向同一个连接写数据还是要给write套一个lock避免交错写入破坏协议。5.2 心跳保活与断线重连客户端锁屏或WiFi切换后TCP连接可能已经死亡服务端却长时间不知道。处理方式有两种Android客户端每隔10秒发一个心跳包服务端在收到业务数据或心跳时刷新该连接的最后活跃时间超过30秒没有读到的连接就主动关闭。服务端清理逻辑可以放在一个Timer线程里扫描clients。Android端的重连逻辑也简单在断线回调里启动一个延迟任务尝试重新connect失败则指数退避比如1秒、2秒、4秒。注意不要在UI线程循环connect会卡界面。把这个重连逻辑放在一个后台Service里App切到后台后仍然可以保持连接。验证这套机制是否生效可以这样测用两个Android模拟器连接同一个PC服务端各自发送消息确认服务端能区分来源然后直接拔掉PC网线再插回观察客户端是否在指数退避后自动恢复连接同时服务端日志里能看到旧连接的关闭记录和新连接的accept记录。测试时在服务端日志里同时观察多条连接的收发记录并用拔网线的方式验证重连退避是否正确。本文还有配套的精品资源点击获取