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通信

在了解TCP协议后我们就可以使用Socket实现简单的消息服务器了,事不宜迟,现在就动手操作!

开始编写

在读懂这些代码之前,最好有C++、java、python的基础,不然下面的代码理解起来会费劲。为了演示Socket通信的能力,这里特意混用了几种语言:Java和Python写客户端,C++写服务端,顺便也能看看不同语言的Socket接口到底差在哪。

数据接收端(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <WINSOCK2.H>
#include "windows.h"

#pragma comment(lib, "ws2_32.lib")
#define _WINSOCK_DEPRECATED_NO_WARNINGS

int main()
{
//定义数据套接字
WSADATA wsaData;
SOCKET Server;
SOCKET Client;
SOCKADDR_IN local;
SOCKADDR_IN client;
//传过来的数据
char Messagesize[1024];
int retu;
int iaddrSize = sizeof(SOCKADDR_IN);
WSAStartup(0x0202, &wsaData);
//设置服务端
Server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
local.sin_family = AF_INET;
//设置端口
local.sin_port = htons(8090);
//得到地址
local.sin_addr.s_addr = htonl(INADDR_ANY);
//监听服务
bind(Server, (struct sockaddr*)&local, sizeof(SOCKADDR_IN));
listen(Server, 1);
//得到客户端
Client = accept(Server, (struct sockaddr*)&client, &iaddrSize);
printf("服务器得到客户端的连接:%s:%d\n", inet_ntoa(client.sin_addr),ntohs(client.sin_port));
//输出得到的内容
while (TRUE) {
retu = recv(Client, Messagesize, 1024, 0);
Messagesize[retu] = '\0';
printf("接收到数据 [%d 字节]: '%s'\n", retu, Messagesize);
}
return 0;
}

代码从头捋一遍。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前面才生效,上面这段代码把它写在了后面,所以inet_ntoa的过时警告其实压不掉。

数据发送客户端1号(Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;
import java.net.Socket;

public class Main {
public static void main(String[] args) {
try {
//配置Socket对象
Socket socket = new Socket("localhost",8090);
//定义输出流对象
PrintWriter printWriter = new PrintWriter(socket.getOutputStream());
//字符串转如Socket发送出去
printWriter.write("Hey!Im java client");
//释放空间
printWriter.flush();
} catch (IOException exception) {
exception.printStackTrace();
}
}
}

Java这边省事多了。new Socket(“localhost”, 8090)这一行构造完,TCP连接就已经建好了(底层就是connect加三次握手),连不上会直接抛IOException被catch住。getOutputStream拿到的是字节流,外面包一层PrintWriter就方便直接写字符串了。这里有个必须注意的点:write只是把内容写进缓冲区,不flush的话数据可能一直卡在本地,程序都结束了对面还没收到,所以flush这行不能省。

另外“localhost”只能连本机,想从别的电脑连过来得换成服务端的局域网IP,跨机器还要留意Windows防火墙,第一次跑一般会弹窗让你放行。这个客户端发完就退出了,Socket跟着进程一起关闭,服务端那边的recv正好收到断开;要是想做一直挂着的聊天客户端,就得把收发放进循环,还要处理对方主动断开的情况。

数据发送客户端2号(Python)

1
2
3
4
5
6
7
8
9
10
11
import socket

s = socket.socket()
s.connect(("127.0.0.1", 8090))

while 1:
s.send("Hi,Im python client".encode("utf-8"))
data = s.recv(1024)
print('recv:',data.decode())

s.close()

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++服务端上体现,两个客户端发来的字符串都被打印了出来。

python

java

总结

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


Socket通信
https://blog.minloha.cn/posts/122220a9e174ed2021122252.html
作者
Minloha
发布于
2021年12月22日
更新于
2021年12月22日
许可协议