Socket通信
TCP/IP
先复习一下TCP/IP协议,网络分为物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。TCP协议作用在传输层,表现出来就是三次握手;IP在网络层负责寻址。归纳一下:TCP是传输层协议,HTTP负责在应用层包装数据,而Socket就是对TCP/IP的封装与使用。实际写代码时一般不会把七层都碰一遍,Socket就夹在应用层和传输层之间,程序通过它来调用下面的TCP/IP。
Socket接口几乎是所有语言都拥有的API(Socket不是协议,是接口),初学者通过Socket可以实现对网络的初步利用与研究,比如局域网内简单的收发数据,这些都是可以自己动手实现的。
TCP协议是所有学习计算机的人必须知道的。三次握手说起来也不复杂:客户端先发一个SYN包,服务器回一个SYN+ACK,客户端再回一个ACK,连接就建好了,之后双方看到的就是一条可靠、有序的字节流。可靠是有代价的,要维护连接状态、要重传、要按序重组,所以TCP比UDP慢一些;UDP的Socket走SOCK_DGRAM,无连接,发出去就不管了,丢了也不重发,适合视频通话这种丢一两帧无所谓的场景。

在了解TCP协议后我们就可以使用Socket实现简单的消息服务器了,事不宜迟,现在就动手操作!
开始编写
在读懂这些代码之前,最好有C++、java、python的基础,不然下面的代码理解起来会费劲。为了演示Socket通信的能力,这里特意混用了几种语言:Java和Python写客户端,C++写服务端,顺便也能看看不同语言的Socket接口到底差在哪。
数据接收端(C++)
1 | |
代码从头捋一遍。WSAStartup(0x0202, &wsaData)是Windows下的Socket初始化,0x0202表示要求Winsock 2.2,Linux上写Socket没有这一步。socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)创建了一个IPv4的、面向连接的TCP套接字,三个参数分别对应地址族、套接字类型和协议。
local.sin_port = htons(8090)里的htons是“主机序转网络序”的意思。不同CPU存多字节整数的方式不一样,网络协议统一规定用大端,所以端口和地址往结构体里塞之前都得转换一下,少了这步端口号就串了。sin_addr填INADDR_ANY表示绑定本机所有网卡,如果只写127.0.0.1,别的机器就连不进来。
bind把地址和套接字绑在一起,listen(Server, 1)开始监听,最后的1是等待连接的队列长度。accept会一直阻塞,直到真有客户端连上来,然后返回一个全新的套接字专门跟这个客户端通信,原来的Server继续负责接待下一个,这一点第一次写Socket的人经常搞混。
printf那行用inet_ntoa和ntohs把客户端的地址、端口还原成人类能读的格式。recv返回实际收到的字节数,Messagesize[retu] = ‘\0’是手动补字符串结束符——recv只搬原始字节,不会替我们在末尾加’\0’,不补的话直接当字符串printf就越界了,这个坑挺隐蔽。
还有两处可以改进:recv返回0表示对面断开了连接,返回-1表示出错,上面这段代码的while(TRUE)里没判断这两种情况,客户端一退出,服务端就会疯狂打印0字节的数据;另外Windows下用完Socket最好补一句WSACleanup()收尾。顺便说一句,_WINSOCK_DEPRECATED_NO_WARNINGS这个宏要写在#include
数据发送客户端1号(Java)
1 | |
Java这边省事多了。new Socket(“localhost”, 8090)这一行构造完,TCP连接就已经建好了(底层就是connect加三次握手),连不上会直接抛IOException被catch住。getOutputStream拿到的是字节流,外面包一层PrintWriter就方便直接写字符串了。这里有个必须注意的点:write只是把内容写进缓冲区,不flush的话数据可能一直卡在本地,程序都结束了对面还没收到,所以flush这行不能省。
另外“localhost”只能连本机,想从别的电脑连过来得换成服务端的局域网IP,跨机器还要留意Windows防火墙,第一次跑一般会弹窗让你放行。这个客户端发完就退出了,Socket跟着进程一起关闭,服务端那边的recv正好收到断开;要是想做一直挂着的聊天客户端,就得把收发放进循环,还要处理对方主动断开的情况。
数据发送客户端2号(Python)
1 | |
Python的写法最直白。socket.socket()不传参数时默认就是AF_INET加SOCK_STREAM,也就是TCP,等价于C++里的socket(AF_INET, SOCK_STREAM, 0)。connect同样会触发三次握手。Python 3里socket只认bytes,所以发送前要用encode(“utf-8”)把字符串转成字节,直接send字符串会报TypeError。
这里有个跑起来才会发现的坑:上面的C++服务端只收不发,Python客户端发完第一句就卡在recv上等回复了,会一直阻塞。想让它正常工作,可以在服务端recv之后加一句send把消息回传,客户端就能收到自己的回声。s.close()写在while外面其实永远执行不到,正常做法是跳出循环再关。
结果
均可在C++服务端上体现,两个客户端发来的字符串都被打印了出来。


总结
这次我们拿三种语言围着同一个8090端口转了一圈,C++做服务端,Java和Python做客户端,数据都能正常送达,说明Socket这层接口在语言之间是相通的,只是封装的写法不一样。基础的东西还是得自己动手跑一遍,光看理论不会知道recv返回0意味着什么、flush忘了会怎样。感兴趣的话可以把服务端改成收一句回一句的聊天程序,把收发循环补全,练手正合适。