socket编程, since 2021-07-04

(2021.07.04 Sun)

socket基本

socket(套接字)是计算机网络节点间和进程间通信的一种方式/约定,网络节点可通过socket发送或接收信息。socket封装和提供了网络协议(栈)的接口,通过接口可控制协议栈的工作,实现跨主机网络通信。

在同一台主机中,进程可以通过唯一进程标识符识别,即PID,而跨主机间的进程用PID不适合。在socket中,不同主机的进程通过网络地址、协议和端口的三元组来唯一标识进程,即(IP, protocol, port)

Unix/Linux的基本哲学是"一切皆文件",文件可以通过"打开"、"读写"、"关闭"的方式进行操作。socket继承了这种哲学。在socket中,网络上的设备都被当做文件,并赋值文件标识符,对这些设备的操作,可类比于对本地磁盘上的文件进行操作。

socket中封装了网络协议的实现。一个典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。

Python、JAVA等都提供了对socket的支持,并提供了专用包。

socket类型

SOCK_STREAM流格式套接字

SOCK_STREAM 是一种可靠的、双向的通信数据流,数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。该格式使用了TCP作为传输层协议,因此提供了可靠通信,即数据的无损和正序。

流格式套接字的内部有一个缓冲区(也就是字符数组),通过 socket 传输的数据将保存到这个缓冲区。接收端在收到数据后并不一定立即读取,只要数据不超过缓冲区的容量,接收端有可能在缓冲区被填满以后一次性地读取,也可能分成好几次读取。

HTTP使用了SOCK_STREAM套接字。

SOCK_DGRAM数据包格式套接字

无连接套接字,其传输层使用了UDP协议。数据传输过程中只管传输数据,不做数据校验,是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。传输时限制数据大小,发送和接收同步。常用于视频会议等场合。

(2021.08.08 Sun)

服务端和客户端创建socket连接的流程-TCP

  1. 服务端调用socket函数创建一个socket,该函数的返回值是socket唯一描述字(socket descriptor)
  2. 服务端调用bind函数将地址族中的地址赋给socket,也就是将服务端的地址绑定到socket,客户端可通过该地址与服务端通信
  3. 服务端调用listen函数监听socket,判断是否有来自客户端的connect申请
  4. 客户端调用connect函数,向服务器发出连接请求
  5. 服务端listen到客户端的connect请求,调用accept函数,通过三次握手建立连接;连接建立完成,可类比文件读写操作,对socket进行操作
  6. 两端通过read/write,或recvmsg/sendmsg函数对socket进行读写操作
  7. 操作完成调用close函数完成TCP连接

注: 在服务端调用accept时,成功连接后会返回一个已完成连接的socket,用来传输数据。监听的socket和用于数据传输的socket是两个socket。

创建socket和通信的流程

socket中三次握手的流程

当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

socket handshake

socket demo, in Python

# 2021.08.08 Sun
# socket demo: tcp

import argparse, socket

def recvall(sock, length):
    data = b''
    while len(data) < length:
        more = sock.recv(length - len(data))
        print(more,'\n')
        if not more:
            # raise EOFError('was expecting %d bytes but only received %d bytes before the socekt closed' % (length, len(data)))
            raise EOFError('was expecting {} bytes but only received {} bytes before the socekt closed'.format(length, len(data)))
        data += more
    return data

def server(interface, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((interface, port))
    sock.listen(1)
    print('listening at ', sock.getsockname())
    while True:
        sc, sockname = sock.accept()
        print('we have accepted a connection from ', sockname)
        print('Socket name: ', sc.getsockname())
        print('Socket peer: ', sc.getpeername())
        # message = recvall(sc, 16)
        message = sc.recv(8096)
        print('Incoming sixteen-octet message: ', repr(message))
        sc.sendall(b'ffff, client')
        sc.close()
        print('Reply sent, socket closed')

def client(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    print('client has been assigned socket name ', sock.getsockname())
    sock.sendall(b'hi server')
    # reply = recvall(sock, 8096)
    reply = sock.recv(8096)
    print('server said ', repr(reply))
    sock.close()

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send and receive over TCP')
    parser.add_argument('role', choices=choices, help='which role to play')
    parser.add_argument('host', help='interface the server listens at; '
                                     ' host the client sends to')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    print('host: ', args.host,', port: ', args.p)
    function(args.host, args.p)

在服务端和客户端可同时使用该脚本。服务端调用方法:

$ python3 socket_tcp.py server '192.168.1.3' # specify host ip

客户端调用方法:

$ python3 socket_tcp.py client '192.168.1.3' # specify server host ip

设置传输字节的长度可运行该程序。

(2021.08.12 Thur)

服务端和客户端创建socket连接的流程-UDP

与TCP相比,UDP没有握手过程,在实现上的服务器端少了accept步骤。

# socket_udp.py
# UDP client and server

import argparse, socket
from datetime import datetime

MAX_BYTES = 65535

def server(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('127.0.0.1', port))
    print('listening at {}'.format(socket.getsockname()))
    while True:
        data, address = sock.recvfrom(MAX_BYTES)
        text = data.decode('ascii')
        print('client at {} says {!r}'.format(address, text))
        text = 'your data was {} bytes long'.format(len(data))
        data = text.encode('ascii')
        sock.sendto(data, address)

def client(port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    text = 'The time is {}'.format(datatime.now())
    data = text.encode('ascii')
    sock.sendto(data, ('127.0.0.1', port))
    print('The OS assigned me the address {}'.format(sock.getsockname()))
    data, address = sock.recvfrom(MAX_BYTES) 
    text = data.decode('ascii')
    print('the server {} replied {!r}'.format(address, text))

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send adn receive UDP locally')
    parser.add_argument('role', choices=choices, help='which role to play')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='UDP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    function(args.p)

在服务器端的输入

$python3 socket_udp.py server
Listening at ('127.0.0.1', 1060)

在客户端的输入

$python3 socket_udp.py client

注意到与TCP不同的是,代码中并没有套接字的connect()函数。如果使用sento()函数,那么每次向服务器发送信息的时候都必须显式的给出服务器的IP地址和端口;而如果使用connec()调用,操作系统事先就已经知道数据包要发送到的远程地址,这样就可以简单的把要发送的数据作为参数传入send()调用,无需重复给出服务器地址。

广播和多播(multicast)

UDP有个特别强大的功能:广播。通过广播,数据包的目标地址被设置为本机连接的整个子网,然后使用物理网卡将数据包广播,就无需复制该数据包并单独将其发送给所有连接到该子网的主机。这里暂只介绍广播。

广播发生在接收广播数据包的服务器发送广播数据包的客户端之间。和前面UDP的例子不同之处在于,使用socket对象时,先调用setsockopt()方法,设置为允许进行广播。

# socket upd broadcast

import argparse, socket

BUFSIZE = 65535

def server(interface, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((interface, port))
    print('Listening for dataframs at {}'.format(sock.getsockname()))
    while True:
        data, address = sock.recvfrom(BUFSIZE)
        text = data.decode('ascii')
        print('the client at {} say: {!r}'.format(address, text))

def client(network, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    text = 'Broadcast datagram.'
    sock.sendto(text.encode('ascii'), (network, port))

if __name__ == '__main__':
    choices = {'client': client, 'server': server}
    parser = argparse.ArgumentParser(description='Send, receive UDP broadcast')
    parser.add_argument('role', choices=choices, help='which role to take')
    parser.add_argument('host', help='interface the server listens at; network the client sends to')
    parser.add_argument('-p', metavar='port', type=int, default=1060, help='UDP port (default 1060)')
    args = parser.parse_args()
    function = choices[args.role]
    function(args.host, args.p)

运行方法
首先使用下面指令运行一到两台网络中的服务器

$python socket_udp_broadcast.py server ""
Listing for broadcasts at ('0.0.0.0', 1060)

保持各服务器运行。在客户端上输入ifconfig找到broadcast地址,比如192.168.1.255,在本地网络上运行

$python socket_udp_broadcast.py client 192.168.1.255

另一种广播的方法<broadcast>,不需要在本地获得广播地址,并且向本机的所有网络端口进行广播。

$python socket_udp_broadcast.py client "<broadcast>"

(2021.08.15 Sun)

Socket参数

在socket初始化过程中,需要对socket做如下设置

import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((host, port))

除了指定了服务器/客户端的IP和port,还需要指定socket方法中的两个参数,即地址族(address family)和套接字类型(socket type)。另有一个隐藏参数时协议(protocol),暂不讨论。

地址族

某个特定的机器可能连接到多个不同类型的网络。对地址族的选择指定了想要进行通信的网络类型。可找出socket的方法并打印以AF_(address family)开头的符号,查看其它选择,诸如AF_APPLETALKAF_BLUETOOTH等。TCP和UDP可用AF_INET

套接字类型

该类型给出了希望在已经选择的网络上使用的特定通信技术。
TCP选SOCK_STREAM,UDP选SOCK_DGRAM。

地址解析

socket.getaddrinfo()用于地址解析。

Reference

1 图片源自网络
2 Brandon R. and etc,诸豪文译,Python网络编程(第3版),中国工信出版社,人民邮电出版社

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容