>백엔드 개발 >파이썬 튜토리얼 >소켓 프로그래밍 실습

소켓 프로그래밍 실습

高洛峰
高洛峰원래의
2016-11-05 09:58:16843검색

Socket은 영어로 "홈(두 항목을 연결하는)"을 의미합니다. 마치 "눈 소켓"을 의미하는 Eye Socket과 마찬가지로 "소켓"을 의미하기도 합니다. 컴퓨터 과학에서 소켓은 일반적으로 연결의 두 끝점을 나타냅니다. 여기서 연결은 유닉스 도메인 소켓과 같은 동일한 시스템에 있을 수도 있고 네트워크 소켓과 같은 다른 시스템에 있을 수도 있습니다.

이 기사에서는 네트워크 모델에서의 위치, API 프로그래밍 패러다임, 일반적인 오류 등을 포함하여 가장 일반적으로 사용되는 네트워크 소켓에 중점을 두고 마지막으로 Python 언어의 소켓 API를 사용하여 몇 가지 실제 예제를 구현합니다. . 소켓은 일반적으로 중국어로 "소켓"으로 번역됩니다. "충실하고 우아한" 번역을 기대하지 않았기 때문에 이 기사에서는 영어 표현을 직접 사용했습니다. 이 기사의 모든 코드는 소켓.py 저장소에서 찾을 수 있습니다.

개요

소켓은 일반적인 기술 사양으로 1983년 버클리 대학에서 4.2BSD Unix용으로 처음 제공한 이후 점차 POSIX 표준으로 발전했습니다. 소켓 API는 애플리케이션이 소켓 기술의 사용을 제어할 수 있도록 운영 체제에서 제공하는 프로그래밍 인터페이스입니다. Unix 철학에는 모든 것이 파일이라는 원칙이 있으므로 소켓과 파일의 API 사용법은 매우 유사합니다. 읽기, 쓰기, 열기, 닫기 등과 같은 작업을 수행할 수 있습니다.

현재 네트워크 시스템은 이론상의 OSI 모델과 업계의 TCP/IP 프로토콜 제품군으로 계층화되어 있습니다. 비교는 다음과 같습니다.

소켓 프로그래밍 실습

각 레이어에는 해당 프로토콜이 있습니다. 소켓 API는 TCP/IP 프로토콜 제품군에 속하지 않으며, TCP/IP 프로토콜 제품군에 속하지 않습니다. 네트워크 프로그래밍을 위한 운영 체제, 애플리케이션 계층과 전송 계층 사이에서 작동하는 인터페이스:

소켓 프로그래밍 실습

우리는 일반적으로 웹사이트에서 사용하는 HTTP 프로토콜과 SMTP 및 IMAP을 검색합니다. 메일 보내기 및 받기는 소켓 API 구성을 기반으로 합니다.

소켓에는 두 가지 필수 구성 요소가 포함되어 있습니다.

주소는 192.168.0.1:80과 같이 IP와 포트로 구성됩니다.

소켓이 사용하는 전송 프로토콜인 프로토콜은 현재 TCP, UDP, 원시 IP의 세 가지 유형이 있습니다.

주소와 프로토콜에 따라 소켓이 결정될 수 있습니다. 한 컴퓨터에 동일한 소켓이 하나만 존재할 수 있습니다. TCP 포트 53의 소켓과 UDP 포트 53의 소켓은 서로 다른 두 개의 소켓입니다.

소켓이 데이터를 전송하는 다양한 방식(사용되는 다양한 프로토콜)에 따라 다음 세 가지 유형으로 나눌 수 있습니다.

"연결 지향" 소켓이라고도 알려진 스트림 소켓 , TCP 프로토콜을 사용하십시오. 실제 통신에 앞서 연결이 필요하고 전송되는 데이터는 특정한 구조를 갖고 있지 않기 때문에 상위 프로토콜에서는 데이터 구분자 자체를 정의해야 하지만 데이터를 신뢰할 수 있다는 장점이 있다.

"무연결" 소켓이라고도 하는 데이터그램 소켓은 UDP 프로토콜을 사용합니다. 실제 통신 전에 연결할 필요가 없습니다. 한 가지 장점은 UDP 데이터 패킷이 자체 구분된다는 것입니다. 즉, 각 데이터 패킷은 데이터의 시작과 끝을 표시합니다. 단점은 데이터가 신뢰할 수 없다는 것입니다.

원시 소켓은 일반적으로 라우터나 기타 네트워크 장치에 사용됩니다. 이러한 종류의 소켓은 TCP/IP 프로토콜 제품군의 전송 계층을 거치지 않고 인터넷 계층에서 애플리케이션 계층으로 직접 전달됩니다. 애플리케이션 계층)이므로 이때 데이터 패킷에는 tcp 또는 udp 헤더 정보가 포함되지 않습니다.

소켓 프로그래밍 실습

Python 소켓 API

Python은 (ip, port) 튜플을 사용하여 소켓의 주소 속성을 나타내고 AF_*를 사용하여 프로토콜 유형을 나타냅니다. .

데이터 통신에는 전송/수신 또는 읽기/쓰기라는 두 가지 동사 세트 중에서 선택할 수 있습니다. 읽기/쓰기 방법은 Java에서도 사용됩니다. 이 방법은 여기에서 자세히 설명하지는 않지만 다음 사항에 유의해야 합니다.

읽기/쓰기 작업은 버퍼가 있는 "파일"을 사용하므로 읽고 쓴 후에 실제로 데이터를 보내거나 읽으려면 플러시 메서드를 호출해야 합니다. 그렇지 않으면 데이터가 버퍼에 유지됩니다.

TCP 소켓

TCP 소켓은 연결 전에 연결을 설정해야 하므로 해당 모드는 UDP 소켓보다 더 책임이 있습니다. 자세한 내용은 다음과 같습니다.

소켓 프로그래밍 실습

각 API의 구체적인 의미는 여기서 설명하지 않습니다. Python 언어로 구현된 에코 서버는 다음과 같습니다.

# echo_server.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
# 设置 SO_REUSEADDR 后,可以立即使用 TIME_WAIT 状态的 socket 
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
sock.bind(('', 5500)) 
sock.listen(5)
def handler(client_sock, addr): 
    print('new client from %s:%s' % addr) 
    msg = client_sock.recv(1024) 
    client_sock.send(msg) 
    client_sock.close() 
    print('client[%s:%s] socket closed' % addr) 
 
if __name__ == '__main__': 
    while 1: 
        client_sock, addr = sock.accept() 
        handler(client_sock, addr)
# echo_client.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
sock.connect(('', 5500)) 
sock.send('hello socket world') 
print sock.recv(1024)

위 단순 에코 서버 코드에서 주의할 점은 서버측 소켓이 SO_REUSEADDR을 1로 설정하여 TIME_WAIT 상태의 소켓을 사용할 수 있다는 점이다. 그럼 TIME_WAIT는 무슨 뜻인가요? 나중에 tcp 상태 변경 다이어그램을 설명할 때 자세히 소개하겠습니다.

UDP 소켓

소켓 프로그래밍 실습

UDP socket server 端代码在进行bind后,无需调用listen方法。

# udp_echo_server.py 
# coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
# 设置 SO_REUSEADDR 后,可以立即使用 TIME_WAIT 状态的 socket 
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
sock.bind(('', 5500)) 
# 没有调用 listen 
 
if __name__ == '__main__': 
    while 1: 
        data, addr = sock.recvfrom(1024) 
 
        print('new client from %s:%s' % addr) 
        sock.sendto(data, addr) 
 
# udp_echo_client.py 
# coding=utf8 
import socket 
 
udp_server_addr = ('', 5500) 
 
if __name__ == '__main__': 
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
    data_to_sent = 'hello udp socket' 
    try: 
        sent = sock.sendto(data_to_sent, udp_server_addr) 
        data, server = sock.recvfrom(1024) 
        print('receive data:[%s] from %s:%s' % ((data,) + server)) 
    finally: 
        sock.close()

常见陷阱

忽略返回值

本文中的 echo server 示例因为篇幅限制,也忽略了返回值。网络通信是个非常复杂的问题,通常无法保障通信双方的网络状态,很有可能在发送/接收数据时失败或部分失败。所以有必要对发送/接收函数的返回值进行检查。本文中的 tcp echo client 发送数据时,正确写法应该如下:

total_send = 0 
content_length = len(data_to_sent) 
while total_send < content_length: 
    sent = sock.send(data_to_sent[total_send:]) 
    if sent == 0: 
        raise RuntimeError("socket connection broken") 
    total_send += total_send + sent

send/recv操作的是网络缓冲区的数据,它们不必处理传入的所有数据。

一般来说,当网络缓冲区填满时,send函数就返回了;当网络缓冲区被清空时,recv 函数就返回。

当 recv 函数返回0时,意味着对端已经关闭。

可以通过下面的方式设置缓冲区大小。

s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, buffer_size)

认为 TCP 具有 framing

TCP 不提供 framing,这使得其很适合于传输数据流。这是其与 UDP 的重要区别之一。UDP 是一个面向消息的协议,能保持一条消息在发送者与接受者之间的完备性。

소켓 프로그래밍 실습

代码示例参考:framing_assumptions

TCP 的状态机

在前面echo server 的示例中,提到了TIME_WAIT状态,为了正式介绍其概念,需要了解下 TCP 从生成到结束的状态机器。(图片来源)

소켓 프로그래밍 실습

这个状图转移图非常非常关键,也比较复杂,我自己为了方便记忆,对这个图进行了拆解,仔细分析这个图,可以得出这样一个结论,连接的打开与关闭都有被动(passive)与主动(active)两种,主动关闭时,涉及到的状态转移最多,包括FIN_WAIT_1、FIN_WAIT_2、CLOSING、TIME_WAIT。

此外,由于 TCP 是可靠的传输协议,所以每次发送一个数据包后,都需要得到对方的确认(ACK),有了上面这两个知识后,再来看下面的图:

소켓 프로그래밍 실습

在主动关闭连接的 socket 调用 close方法的同时,会向被动关闭端发送一个 FIN

对端收到FIN后,会向主动关闭端发送ACK进行确认,这时被动关闭端处于 CLOSE_WAIT 状态

当被动关闭端调用close方法进行关闭的同时向主动关闭端发送 FIN 信号,接收到 FIN 的主动关闭端这时就处于 TIME_WAIT 状态

这时主动关闭端不会立刻转为 CLOSED 状态,而是需要等待 2MSL(max segment life,一个数据包在网络传输中最大的生命周期),以确保被动关闭端能够收到最后发出的 ACK。如果被动关闭端没有收到最后的 ACK,那么被动关闭端就会重新发送 FIN,所以处于TIME_WAIT的主动关闭端会再次发送一个 ACK 信号,这么一来(FIN来)一回(ACK),正好是两个 MSL 的时间。如果等待的时间小于 2MSL,那么新的socket就可以收到之前连接的数据。

前面 echo server 的示例也说明了,处于 TIME_WAIT 并不是说一定不能使用,可以通过设置 socket 的 SO_REUSEADDR 属性以达到不用等待 2MSL 的时间就可以复用socket 的目的,当然,这仅仅适用于测试环境,正常情况下不要修改这个属性。

实战

HTTP UA

http 协议是如今万维网的基石,可以通过 socket API 来简单模拟一个浏览器(UA)是如何解析 HTTP 协议数据的。

#coding=utf8 
import socket 
 
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
baidu_ip = socket.gethostbyname(&#39;baidu.com&#39;) 
sock.connect((baidu_ip, 80)) 
print(&#39;connected to %s&#39; % baidu_ip) 
 
req_msg = [ 
    &#39;GET / HTTP/1.1&#39;, 
    &#39;User-Agent: curl/7.37.1&#39;, 
    &#39;Host: baidu.com&#39;, 
    &#39;Accept: */*&#39;, 
] 
delimiter = &#39;\r\n&#39; 
 
sock.send(delimiter.join(req_msg)) 
sock.send(delimiter) 
sock.send(delimiter) 
 
print(&#39;%sreceived%s&#39; % (&#39;-&#39;*20, &#39;-&#39;*20)) 
http_response = sock.recv(4096) 
print(http_response)

运行上面的代码可以得到下面的输出

--------------------received-------------------- 
HTTP/1.1 200 OK 
Date: Tue, 01 Nov 2016 12:16:53 GMT 
Server: Apache 
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT 
ETag: "51-47cf7e6ee8400" 
Accept-Ranges: bytes 
Content-Length: 81 
Cache-Control: max-age=86400 
Expires: Wed, 02 Nov 2016 12:16:53 GMT 
Connection: Keep-Alive 
Content-Type: text/html 
 
<html> 
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/"> 
</html>

http_response是通过直接调用recv(4096)得到的,万一真正的返回大于这个值怎么办?我们前面知道了 TCP 协议是面向流的,它本身并不关心消息的内容,需要应用程序自己去界定消息的边界,对于应用层的 HTTP 协议来说,有几种情况,最简单的一种时通过解析返回值头部的Content-Length属性,这样就知道body的大小了,对于 HTTP 1.1版本,支持Transfer-Encoding: chunked传输,对于这种格式,这里不在展开讲解,大家只需要知道, TCP 协议本身无法区分消息体就可以了。对这块感兴趣的可以查看 CPython 核心模块 http.client

Unix_domain_socket

UDS 用于同一机器上不同进程通信的一种机制,其API适用与 network socket 很类似。只是其连接地址为本地文件而已。

代码示例参考:uds_server.py、uds_client.py

ping

ping 命令作为检测网络联通性最常用的工具,其适用的传输协议既不是TCP,也不是 UDP,而是 ICMP,利用 raw sockets,我们可以适用纯 Python 代码来实现其功能。

代码示例参考:ping.py

netstat vs ss

netstat 与 ss 是类 Unix 系统上查看 Socket 信息的命令。netstat 是比较老牌的命令,我常用的选择有

-t,只显示 tcp 连接

-u,只显示 udp 连接

-n,不用解析hostname,用 IP 显示主机,可以加快执行速度

-p,查看连接的进程信息

-l,只显示监听的连接

ss 是新兴的命令,其选项和 netstat 差不多,主要区别是能够进行过滤(通过state与exclude关键字)。

$ ss -o state time-wait -n | head 
Recv-Q Send-Q             Local Address:Port               Peer Address:Port 
0      0                 10.200.181.220:2222              10.200.180.28:12865  timer:(timewait,33sec,0) 
0      0                      127.0.0.1:45977                 127.0.0.1:3306   timer:(timewait,46sec,0) 
0      0                      127.0.0.1:45945                 127.0.0.1:3306   timer:(timewait,6.621ms,0) 
0      0                 10.200.181.220:2222              10.200.180.28:12280  timer:(timewait,12sec,0) 
0      0                 10.200.181.220:2222              10.200.180.28:35045  timer:(timewait,43sec,0) 
0      0                 10.200.181.220:2222              10.200.180.28:42675  timer:(timewait,46sec,0) 
0      0                      127.0.0.1:45949                 127.0.0.1:3306   timer:(timewait,11sec,0) 
0      0                      127.0.0.1:45954                 127.0.0.1:3306   timer:(timewait,21sec,0) 
0      0               ::ffff:127.0.0.1:3306           ::ffff:127.0.0.1:45964  timer:(timewait,31sec,0)

这两个命令更多用法可以参考:

SS Utility: Quick Intro

10 basic examples of linux netstat command

总结

我们的生活已经离不开网络,平时的开发也充斥着各种复杂的网络应用,从最基本的数据库,到各种分布式系统,不论其应用层怎么复杂,其底层传输数据的的协议簇是一致的。Socket 这一概念我们很少直接与其打交道,但是当我们的系统出现问题时,往往是对底层的协议认识不足造成的,希望这篇文章能对大家编程网络方面的程序有所帮助。


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.