python使用Socket获取IP地址-顺便初学Socket

偶然看的一个使用Socket获取本机IP的方式,感觉还挺有意思的,于是便有了这篇Blog,同时通过这个简单的项目,初步了解下在网络通信里面经常看的的Socket。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from contextlib import closing
import socket

def get_host_ip():
ip = None
with closing(socket.socket(socket.AF_INET,socket.SOCK_STREAM)) as _socket:
_socket.connect(('1.1.1.1', 80))
ip = _socket.getsockname()[0]
print(ip) # 192.168.1.5


if __name__ == '__main__':
get_host_ip()

  • 从代码可以看出核心是socket模块,无论是connect还是getsockname都是其中的方法。因此我们最好可以先简单学习一下—Socket(套接字)

  • 在学习过程中,有这样一个概念—当你使用connect()方法将套接字连接到远程主机时,套接字对象的本地IP地址是由操作系统自动分配的。操作系统通常会使用本机上的一个可用IP地址作为套接字对象的本地IP地址,这个IP地址通常是本机的一个局域网IP地址或公网IP地址,取决于本机与远程主机之间的网络环境。

  • 同时,再看getsockname方法的作用—用于获取套接字的本地地址和端口号。它返回一个元组,包含套接字绑定的本地地址和端口号。这个本地地址可以用于告诉其他套接字如何连接到它。

  • 这样一来,上面代码的作用就知道了,先创建一个Socket对象_socket,随后将其与('1.1.1.1', 80)建立连接,客户端发送的是一个 TCP 连接请求。这里的80指的是端口号,一般是对于Web服务的端口—在使用网络编程时,便于正确地指定端口号,因此需要对其有一定的了解,比如DNS使用的53端口。

  • 然后我可以通过_socket.getsockname(),获取操作系统自动给套接字分配的ip地址,通常是本机的一个局域网IP地址或公网IP地址。

可以注意到,在上面代码中使用的with语句与我们平时见到的不一样,它后面还使用了contextlib.closing()方法,这是为什么呢?

contextlib.closing() 方法可以将一个对象封装成上下文管理器,使其在使用完毕后自动关闭。而with 语句是一种上下文管理器,可以自动管理资源的生命周期,一旦代码块执行完毕,无论是否发生异常,with语句会自动调用资源的__exit__()方法来关闭资源。

并不是所有的资源都是上下文管理器,也就是说,并不是所有的资源都可以直接使用with语句来关闭。这时就**可以使用contextlib.closing()方法来将资源包装成上下文管理器,以便能够在with语句中使用

python官方关于socket的介绍可以看这里

后续,基于上面所使用到的内容,初步学习一下。

with语句结合contextlib.closing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# `contextlib.closing(xxx)`的原理如下

class closing(object):
"""Context to automatically close something at the end of a block.
Code like this:
with closing(<module>.open(<arguments>)) as f:
<block>
is equivalent to this:
f = <module>.open(<arguments>)
try:
<block>
finally:
f.close()
"""
def __init__(self, thing):
self.thing = thing
def __enter__(self):
return self.thing
def __exit__(self, *exc_info):
self.thing.close()
  • closing 类包含了一个构造方法和两个特殊方法:__enter____exit____init__ 方法用于初始化 closing 对象,接受一个参数 thing,表示需要关闭的对象。
  • __enter__ 方法返回 thing,即需要关闭的对象,表示进入 with 语句块时要执行的操作。
  • __exit__ 方法在 with 语句块执行完毕后被调用,表示退出 with 语句块时要执行的操作。在 __exit__ 方法中,调用 thing.close() 方法可以关闭 thing 对象,释放占用的资源。
  • 使用 closing 结合with可以将代码从繁琐的 try/finally 代码块中解放出来.

到底什么是socket?

Socket是一个抽象层,它提供了一种通用的接口,让应用程序可以使用不同的协议进行网络通信。

我们先看wiki对socket的描述,这可能需要对于计算机网络有一个初步认识 — socket是一种操作系统提供的进程间通信机制

网络套接字(英语:Network socket;又译网络套接字、网络接口、网络插槽)在计算机科学中是电脑网络中进程间资料流的端点。使用以网际协议(Internet Protocol)为通信基础的网络套接字,称为网际套接字(Internet socket)。因为网际协议的流行,现代绝大多数的网络套接字,都是属于网际套接字。

socket是一种操作系统提供的进程间通信机制。

在操作系统中,通常会为应用程序提供一组应用程序接口(API),称为套接字接口(英语:socket API)。应用程序可以通过套接字接口,来使用网络套接字,以进行资料交换。最早的套接字接口来自于4.2 BSD,因此现代常见的套接字接口大多源自Berkeley套接字(Berkeley sockets)标准。

在套接字接口中,以IP地址及端口组成套接字地址(socket address)。远程的套接字地址,以及本地的套接字地址完成连线后,再加上使用的协议(protocol),这个五元组(fiveelement tuple),作为套接字对(socket pairs),之后就可以彼此交换资料。例如,在同一台计算机上,TCP协议与UDP协议可以同时使用相同的port而互不干扰。 操作系统根据套接字地址,可以决定应该将资料送达特定的进程或线程。这就像是电话系统中,以电话号码加上分机号码,来决定通话对象一般。

是不是看完有点迷糊?我们形象的理解下。

想象一下你和你的朋友们在玩一个网络游戏,你们需要在游戏中进行实时的聊天和数据交换。这时,每个人的电脑就像是一个节点,而节点之间的数据交换需要通过网络进行。

为了实现这个数据交换,每个电脑上的游戏程序需要使用套接字接口来创建一个socket。这个socket会有一个唯一的IP地址和端口号,表示这个socket的地址。在这个游戏中,每个玩家都需要创建一个socket,使得他们能够相互交换数据。

当你发送一条消息给你的朋友时,你的游戏程序会将这条消息写入你创建的socket中,然后通过网络将这条消息发送给你的朋友的电脑。当你的朋友的电脑接收到这条消息时,他的游戏程序会从他创建的socket中读取这条消息,并且在游戏中显示出来。

这里的socket就像是一个管道,连接了两个电脑上的游戏程序,使得它们能够在网络中相互交换数据。而socket地址就像是这个管道的两端,每个电脑上的游戏程序都需要知道自己的socket地址和对方的socket地址,才能够建立起通信连接。

而在上面的例子中,我与朋友在实际应用中就对应着客户端与服务端,而socket地址由ip地址与端口组成,它处理的是进程之间的通信。

简单使用socket

创建socket

在使用socket之前我们需要使用socket提供的socket()方法创建一个Socket。例如,以下代码创建一个TCP套接字:

1
2
3
4
import socket   
# 创建一个TCP套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Socket Created'
  • socket.AF_INET是一个常量,表示使用IPV4协议族的套接字。用于指定套接字使用IPv4地址和端口号。
  • SOCK_STREAM表示使用TCP协议进行通讯。如果这里是SOCK_DGRAM则表示使用UDP协议进行通信,创建的也是UDP套接字

错误处理,当socket.socket()函数执行失败时,会抛出一个socket.error异常,异常的内容就是一个元组,包含两个值,第一个值是一个错误码(error code),第二个值是一个错误信息(error message)。

1
2
3
4
5
6
7
8
9
10
11
12

import socket #for sockets
import sys #for exit

try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error, msg:
print 'Failed to create socket. Error code: ' + str(msg[0]) + ' , Error message : ' + msg[1]
sys.exit();

print 'Socket Created'

客户端Socket操作

在客户端,需要创建一个套接字,并指定服务器的地址和端口来连接到服务器。

获取远程主机IP地址

在连接到服务器之前,客户端需要获取服务器的IP地址。

可以使用socket.gethostbyname()函数来获取远程主机的IP地址,如下所示:

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
import socket   #for sockets
import sys #for exit

try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
sys.exit()

print('Socket Created')

host = 'www.google.net'

try:
remote_ip = socket.gethostbyname( host )

except socket.gaierror:
#could not resolve
print('Hostname could not be resolved. Exiting')
sys.exit()

print( 'Ip address of ' + host + ' is ' + remote_ip)

# Socket Created
# Ip address of www.baidu.com is 36.152.44.96

gethostbyname()方法是用于将主机名解析为IP地址。它接受一个主机名作为参数,并返回该主机名对应的IP地址。这个IP地址可以用于创建套接字,以便连接到远程服务器或绑定到本地地址。

连接到服务器

  • 首先,客户端需要连接到服务器,以建立网络连接。

  • 在建立连接前,需要知道服务器的IP地址和端口号。

  • 客户端可以使用socket.connect()函数来连接服务器。

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
import socket   #for sockets
import sys #for exit

try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
sys.exit()

print('Socket Created')

host = 'www.baidu.com'
port = 80

try:
remote_ip = socket.gethostbyname( host )

except socket.gaierror:
#could not resolve
print('Hostname could not be resolved. Exiting')
sys.exit()

print( 'Ip address of ' + host + ' is ' + remote_ip)

#Connect to remote server
s.connect((remote_ip , port))
print('Socket Connected to ' + host + ' on ip ' + remote_ip)

# Socket Created
# Ip address of www.baidu.com is 36.152.44.96
# Socket Connected to www.baidu.com on ip 36.152.44.96
  • 注意,connect()方法需要传入一个元组(host, port),其中host是服务器的IP地址,port是服务器的端口号。
  • connect()方法是socket模块中用于创建TCP连接的方法之一。它的作用是连接到一个远程服务器并与之建立TCP连接。

向服务器端发送数据

  • 连接成功后,客户端可以向服务器发送数据。
  • 可以使用socket.send()函数来发送数据,如下所示:
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
42
43
44
45
46
import socket   #for sockets
import sys #for exit

try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
sys.exit()

print('Socket Created')

host = 'www.baidu.com'
port = 80

try:
remote_ip = socket.gethostbyname( host )

except socket.gaierror:
#could not resolve
print('Hostname could not be resolved. Exiting')
sys.exit()

print( 'Ip address of ' + host + ' is ' + remote_ip)

#Connect to remote server
s.connect((remote_ip , port))
print('Socket Connected to ' + host + ' on ip ' + remote_ip)



#Send some data to remote server
message = "'GET / HTTP/1.1\r\nHost: baidu.com\r\n\r\n'"
message = message.encode()

try:
#Set the whole string
s.send(message)
except socket.error:
#Send failed
print('Send failed')
sys.exit()

print('Message send successfully')



  • 在这个例子中,客户端连接到了baidu的端口号80,并使用send()方法向服务器发送了一个消息,消息内容是Hello, server!
  • 需要注意的是,如果使用的是python3,如果不加message = message.encode()将str转换为byte,可能会出现报错TypeError: a byteslike object is required, not ‘str’
  • 这是因为Python3中,str 类型和 unicode 类型是同一种类型。Python2中,str 类型和 bytes 类型是同一种类型。

从服务器端接收数据

客户端可以使用socket.recv()函数从服务器端接收数据,如下所示:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import socket   #for sockets
import sys #for exit

try:
#create an AF_INET, STREAM socket (TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except socket.error:
sys.exit()

print('Socket Created')

host = 'www.baidu.com'
port = 80

try:
remote_ip = socket.gethostbyname( host )

except socket.gaierror:
#could not resolve
print('Hostname could not be resolved. Exiting')
sys.exit()

print( 'Ip address of ' + host + ' is ' + remote_ip)

#Connect to remote server
s.connect((remote_ip , port))
print('Socket Connected to ' + host + ' on ip ' + remote_ip)



#Send some data to remote server
message = "'GET / HTTP/1.1\r\nHost: baidu.com\r\n\r\n'"
message = message.encode()

try:
#Set the whole string
s.send(message)
except socket.error:
#Send failed
print('Send failed')
sys.exit()

print('Message send successfully')

#Now receive data
reply = s.recv(4096)
re_list = reply.split('\r\n'.encode())
for x in re_list:
print(x)

'''
Socket Created
Ip address of www.baidu.com is 36.152.44.95
Socket Connected to www.baidu.com on ip 36.152.44.95
Message send successfully
b'HTTP/1.1 302 Found'
b'Bdpagetype: 3'
b'ContentLength: 154'
b"ContentSecurityPolicy: frameancestors 'self' https://chat.baidu.com http://mirrorchat.baidu.com https://fjchat.baidu.com https://hbachat.baidu.com https://hbechat.baidu.com https://njjschat.baidu.com https://njchat.baidu.com https://hnachat.baidu.com https://hnbchat.baidu.com http://debug.baiduint.com;"
b'ContentType: text/html'
b'Date: Sun, 23 Jul 2023 15:09:34 GMT'
b'Location: https://www.baidu.com/search/error.html'
b'Server: BWS/1.1'
b'SetCookie: BDSVRTM=0; path=/'
b'Traceid: 1690124974056851201016077367759043291635'
b'XUaCompatible: IE=Edge,chrome=1'
b''
b'<html>'
b'<head><title>302 Found</title></head>'
b'<body bgcolor="white">'
b'<center><h1>302 Found</h1></center>'
b'<hr><center>nginx</center>'
b'</body>'
b'</html>'
b''

进程已结束,退出代码0

'''
  • 客户端发送了一条HTTP请求,请求内容是GET / HTTP/1.1\r\nHost: baidu.com\r\n\r\n。关于该请求的含义需要去了解HTTP协议,这里不过多赘述
    collapsed:: true
  • 请求行:GET / HTTP/1.1,其中GET表示请求方法,/表示请求的URI路径,HTTP/1.1表示使用的HTTP协议版本。
  • 请求头:Host: baidu.com,其中Host是HTTP/1.1协议中的一个必选请求头字段,用于指定请求的目标服务器的域名或IP地址。
  • 空行:\r\n表示一个空行,用于分隔请求头和请求体。由于这个请求没有请求体,因此空行后面没有数据。
  • 由于HTTP协议中使用\r\n作为行结束符,因此请求报文中每行结尾都需要添加\r\n。在请求头和请求体之间需要添加一个空行,即两个\r\n,表示请求头已经结束,后面没有请求体数据。
  • 这条请求会向baidu.com的默认HTTP端口80发起请求,要求获取主页内容。然后,客户端使用client_socket.recv()方法从服务器接收了一条消息,数据大小为4096字节。最后,客户端在控制台输出了从服务器接收到的数据。
  • 上述reply后续的处理是为了便于人类阅读加上的。

关闭socket

完成数据交换后,服务器需要关闭socket,以释放网络资源。可以使用socket.close()函数来关闭socket

1
s.close()

服务器端Socket操作

  • 在服务器端,需要创建一个套接字并将其绑定到一个特定的网络地址,以便客户端可以连接到它。

服务器端绑定Socket

  • 在创建完套接字后,我们可以使用bind()方法将套接字绑定到一个特定的IP地址和端口。
  • 例如,以下代码将TCP套接字绑定到本地主机的8000端口
1
2
3
4
5
6
7
import socket

# 创建一个TCP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 将套接字绑定到本地主机的8000端口
sock.bind(('127.0.0.1', 8000))
  • 使用的方法便是bind(),其用于将一个套接字绑定到一个具体的IP地址和端口号上,从而使得其他套接字可以通过该地址和端口号找到这个套接字并与之通信。

Socket侦听连接

  • 接下来,我们可以使用listen()方法开始监听连接请求。
  • 例如,以下代码开始监听TCP连接请求:
1
2
3
4
5
6
7
8
9
10
import socket

# 创建一个TCP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 将套接字绑定到本地主机的8000端口
sock.bind(('127.0.0.1', 8000))

# 开始监听连接请求
sock.listen(1)

接受连接

  • 在监听请求期间,我们可以使用accept()方法接受客户端的连接请求。
  • 例如,以下代码接受一个TCP连接请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import socket

# 创建一个TCP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 将套接字绑定到本地主机的8000端口
sock.bind(('127.0.0.1', 8000))

# 开始监听连接请求
sock.listen(1)

# 接受客户端的连接请求
client_sock, client_addr = sock.accept()

# 输出客户端的地址信息
print('客户端地址:', client_addr)

服务器端与客户端的异同

  • 创建和绑定:在服务器端,需要创建一个套接字并将其绑定到一个特定的网络地址,以便客户端可以连接到它。而在客户端,只需要创建一个套接字,并指定服务器的地址和端口来连接到服务器。
  • 监听和接受连接:服务器端需要调用listen()函数来监听传入的连接请求。一旦有客户端请求连接,服务器端会调用accept()函数来接受连接,并创建一个新的套接字来处理与该客户端的通信。而客户端只需要调用connect()函数来连接到服务器。
  • 数据交换:在服务器端,可以同时处理多个客户端的连接请求,并与每个客户端进行数据交换。服务器端可以使用多线程或多进程来实现并发处理。而客户端通常只与服务器进行一对一的通信。
  • 关闭连接:在服务器端,当与客户端的通信结束后,需要调用close()函数来关闭与该客户端的连接。而客户端在完成通信后,也需要调用close()函数来关闭与服务器的连接。

常用方法语法

  • socket: socket(family, type, proto)
    • 创建一个新的套接字对象其中
    • family 指定地址族(如 AF_INET 表示 IPv4)
    • type 指定套接字类型(如 SOCK_STREAM 表示 TCP 套接字)
    • proto 指定协议类型(如 IPPROTO_TCP 表示 TCP 协议)。
  • connect: connect(address)
    • 连接到指定的地址
    • address 是一个表示远程主机地址和端口号的元组。
  • bind: bind(address)
    • 绑定指定的地址到套接字
    • address 是一个表示本地主机地址和端口号的元组。
  • gethostbyname: gethostbyname(hostname)
    • 解析指定主机名的 IP 地址,返回一个表示 IP 地址的字符串。
  • gethostname: gethostname()
    • 返回本地主机名。
  • send: send(data)
    • 发送数据到连接的套接字,其中 data 是要发送的数据。
  • recv: recv(bufsize)
    • 从连接的套接字接收数据,其中 bufsize 是要接收的最大字节数。
  • listen: listen(backlog)
    • 开始监听连接,其中 backlog 指定连接请求队列的最大长度。

理解时遇到的一些问题

  • 创建的套接字对象的ip地址是不是就是本机的?
    • 在使用socket.socket(socket.AF_INET, socket.SOCK_STREAM)创建套接字对象时,它并没有绑定到任何本地地址或端口上。因此,在调用bind()方法之前,套接字对象并没有本地IP地址。
    • 当调用bind()方法将套接字对象绑定到一个本地IP地址和端口号上时,它的本地IP地址就是你指定的本地IP地址。如果你在调用bind()方法时将IP地址参数设置为'',则表示将套接字绑定到所有可用的IP地址上,这时套接字的本地IP地址将是本机的一个有效IP地址。
    • 另外,当使用connect()方法将套接字连接到远程主机时,套接字对象的本地IP地址是由操作系统自动分配的。操作系统通常会使用本机上的一个可用IP地址作为套接字对象的本地IP地址,这个IP地址通常是本机的一个局域网IP地址或公网IP地址,取决于本机与远程主机之间的网络环境。
    • 因此,套接字对象的本地IP地址是否就是本机的IP地址,取决于你如何使用套接字对象,并且在使用套接字对象之前是否将其绑定到本地地址和端口号上。
  • gethostbynamegetsockname之间存在什么关系呢?
    • gethostbyname()方法是用于将主机名解析为IP地址。它接受一个主机名作为参数,并返回该主机名对应的IP地址。这个IP地址可以用于创建套接字,以便连接到远程服务器或绑定到本地地址。
    • getsockname()方法是用于获取套接字的本地地址和端口号。它返回一个元组,包含套接字绑定的本地地址和端口号。这个本地地址可以用于告诉其他套接字如何连接到它。

reference

-------------已经到底啦!-------------

欢迎关注我的其它发布渠道