|
一个TCP回显服务器,编程几十行代码,实例就能让你直观看到网络编程的编程核心逻辑。别急着背API,实例先跑通一个最简单的编程例子,比什么都管用。实例 我用Python写,编程因为它的实例socket模块最直白,几乎就是编程C语言的翻版,但省去了头疼的实例结构体和指针。你打开终端,编程先起一个服务端: import socket server = socket.socket(socket.AF_INET,实例 socket.SOCK_STREAM) server.bind(('127.0.0.1', 8888)) server.listen(5) print("等待连接...") while True: conn, addr = server.accept() print(f"客户端 { addr} 来了") data = conn.recv(1024) if data: conn.sendall(data) # 原样返回 conn.close() 这段代码干的事很朴素:创建socket,绑定到本机8888端口,编程监听,实例然后死循环里等客户端来敲门。编程accept()会卡住,直到有连接进来才往下走。每次接受一个连接,收数据,原封不动发回去,然后关闭。注意那个listen(5),意思是内核里排队的连接最多5个,超出就拒绝。这个数字不是随便拍的,高并发场景下得调,但本地测试5就够了。 客户端那边更简单: import socket client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect(('127.0.0.1', 8888)) client.sendall(b'hello, socket') response = client.recv(1024) print(response.decode()) client.close() 跑起来,服务端打印“客户端来了”,客户端打印“hello, socket”。就这几行,你已经完成了TCP三次握手、数据收发、四次挥手全过程——只是这些动作都被内核悄悄处理了,你只看到两个函数调用。 但这里有个坑,新手常踩。recv(1024)不是说你一次就能收到1024字节。TCP是流协议,数据像水管里的水,没有边界。你发一个“hello”,可能一次全到,也可能分两次到。更糟的是,如果你发的数据超过1024字节,服务端只读前1024,剩下的还留在内核缓冲区里,下次recv才能读到。所以真实项目里,你得自己定义消息边界,比如用固定长度头、换行符,或者JSON格式。 我见过最蠢的写法,是在recv外面套个while True,循环读到空为止。这逻辑在客户端主动关闭连接时没问题,因为服务端recv返回空字节串表示对方关了。但如果对方一直不关连接,只是没数据发,recv会永远阻塞在那,程序就卡死了。你得设超时,或者用非阻塞模式,再或者用select轮询——这就复杂起来了,但也是从玩具走向实用的第一步。 说个我自己的经历。第一次写socket程序,我拿它做一个局域网文件传输工具。服务端接收文件内容,写到磁盘。客户端发送时,我图省事,直接sendall(file_bytes),结果大文件传一半就断了。后来才明白,sendall只保证把字节全部发出,但不保证接收端一次能收完。文件5MB,服务端recv(1024)得循环5000多次,每次都得检查收到的长度,累加,直到等于文件大小。那会儿我写了个循环,用total_received计数,直到收到文件末尾标志才停。丑是丑了点,但能用。 另一个常见问题,是端口被占用。你上次跑服务端没关干净,再启动时报Address already in use。解决办法是在bind之前加一行server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)。这行代码的意思是告诉内核,端口处于TIME_WAIT状态时也能重新绑定。别问为什么,记着用就行,等你看完TCP协议状态图就懂了。 最后说点实在的。如果你只是想写个内部工具,Python的socket完全够用。但要是做高并发服务,别自己折腾多线程或者select了,直接用asyncio或者Twisted,甚至上gRPC。底层原理你懂个大概就行,真正干活时框架帮你处理了99%的边界情况。socket就像手动挡汽车——学车时用,考完驾照谁还天天开手动挡?但你要是连离合都不认识,自动挡坏了你就只能干瞪眼。 所以,动手敲一遍上面的代码,改改端口,发个中文试试,故意发超过1024字节的数据看会发生什么。踩过坑,你才算真会了。 |