使用socket可以进行基于套接字的底层网络编程。

 

socket对象是网络通信的基础,相当于一个管道连接了发送端和接收端,并在两者之间相互传递数据。

 

五子棋游戏介绍:程序分为客户端和服务端。服务器首先启动,客户端寻找连接成功后,开始游戏。

客户端运行后,输入服务端iip地址,如果连接成功,可以在轮到自己下棋时行动,也可以通过退出游戏按钮退出。

 

进行网络编程,要对网络一些协议有认识。

端口的作用:在两台计算机通信时,只有ip地址是不够的,因为同一台计算机运行着多个网络程序(如qq、浏览器、微信等)。一个ip数据包到来后,到底是发给qq还是浏览器或微信。就需要通过端口号进行区分。

socket是tcp/ip网络最通用的API,任何网络通信都是靠socket来完成的。

 

创建TCP socket的语法如下

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

socket.AF_INET:用于服务器之间的通信,ipv4协议。

socket.SOCK_STREAM:用于TCP协议。

 

创建UDP socket的语法如下

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

sock.SOCK_DGRAM:用于UDP协议。

 

对于tcp,首先服务器启动,服务器和客户机都必须调用socket()建立一个套接字socket,服务器调用bind()将套接字与本机指定端口绑定在一起,在调用listen()使套接字处于一种被动的准备接收的状态。然后客户机建立套接字便可以通过调用connect()与服务器建立连接,服务器可以调用accept()来接收客户机连接,然后继续监听指定端口,并发出阻塞,直到下一个请求出现,从而实现连接多个客户机。建立连接后,客户机和服务器之间就可以发送和接收数据。数据结束传送后,双方调用close()关闭套接字。

对于UDP,客户机通过sendto()给服务器发送数据,服务器调用revcfrom(),等待客户端传来的数据。服务器根据recvfrom()给客户机应答。

 

实例:tcp客户端程序:连接当当网

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建socket对象,tcp

s.connect(('www.dangdang.com', 80))  # 主动建立当当网连接,一般格式为元祖(hostname,port)

s.send(b'GET / HTTP/1.1\r\nHost: www.dangdang.com\r\nConnection: close\r\n\r\n')  # send发送括号中的请求

buffer = []
while True:
    d = s.recv(1024)  # 每次最多接收服务器端1k的数据
    if d:
        buffer.append(d)  # 加入到buffer列表
    else:
        break  # 返回空数据,接收完毕退出循环
data = b''.join(buffer)  # b表示空字节,join()是连接列表的函数,buffer是一个字节串列表,这句就是是用空字节把buffer字节列表连接在一起成为新字符串
s.close()  # 关闭连接

header, html = data.split(b'\r\n\r\n', 1)  # 以\r\n\r\n分隔一次,
print(header.decode('utf-8'))  # 打印头内容

with open('当当.html', 'wb') as f:  # 新建一个文档"当当.html"
    f.write(html)  # 具体html文件写入文档中

结果为 :

打印头文件内容

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 29 Nov 2020 06:37:39 GMT
Content-Type: text/html; charset=GBK
Transfer-Encoding: chunked
Connection: close

在当前目录生成了当当.html,内容略

 

如果想访问https安全协议的网站如新浪,需要ssl协议,将上述代码改为:

import socket
import ssl  # ssl协议需要ssl模块

# s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建socket对象,tcp
sock = ssl.wrap_socket(socket.socket())  # 使用https的socket对象
sock.connect(('www.sina.com.cn', 443))  # https端口为443

sock.send(b'GET / HTTP/1.1\r\nHost: www.sina.com.cn\r\nConnection: close\r\n\r\n')  # send发送括号中的请求

buffer = []
while True:
    d = sock.recv(1024)  # 每次最多接收服务器端1k的数据
    if d:
        buffer.append(d)  # 加入到buffer列表
    else:
        break  # 返回空数据,接收完毕退出循环
data = b''.join(buffer)  # b表示空字节,join()是连接列表的函数,buffer是一个字节串列表,这句就是是用空字节把buffer字节列表连接在一起成为新字符串
sock.close()  # 关闭连接

header, html = data.split(b'\r\n\r\n', 1)  # 以\r\n\r\n分隔一次,
print(header.decode('utf-8'))  # 打印头内容

with open('sina.html', 'wb') as f:
    f.write(html)  # 具体html文件写入文档中

 

实例:tcp服务端程序

tcp服务端相比tcp客户端程序要复杂一些。服务端首先要绑定一个端口用于监听其他客户端的连接。同时服务器还要相应多个客户端的请求,因此每个连接都需要一个新的进程或线程来处理。

编写一个简单的tcp服务端程序,接收客户端请求,将客户端发过来的字符加上Hello再发送回去。

import socket
import time
import threading


def tcplink(sock, addr):
    print("接受一个来自%s:%s的连接请求" % addr)
    sock.send(b'Welcome!')  # 发送给客户端welcome信息
    while True:
        data = sock.recv(1024)  # 接收客户端信息
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
            break  # 如果没有数据或者接受到exit,则终止接受
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))  # 收到的信息加上hello在发回去
    sock.close()
    print("来自%s:%s的连接关闭了" % addr)


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8888))  # 绑定并监听本机8888端口,只有本机客户端才能连接
s.listen(5)  # 连接最大数量为5
print("等待客户连接:")
while True:
    sock, addr = s.accept()  # 接受一个新链接,accpet()会等待并返回一个客户端的连接
    t = threading.Thread(target=tcplink, args=(sock, addr))  # 创建新线程处理tcp连接
    t.start()

此时需要一个测试程序:

import socket
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(('127.0.0.1',8888)) #建立链接
print(s.recv(1024).decode('utf-8'))
for data in [b'Michael',b'Tracy',b'Sarah']:
    s.send(data)
    print(s.recv(1024).decode('utf-8'))
s.send(b'exit')
s.close()

注意服务器一个端口被socket绑定后,就不能被其他socket绑定。客户端连接后关闭,服务器端不会自己退出。

 

UDP编程

之前都是tcp连接方式,udp不需要建立连接,只要知道对方的ip地址和端口号就可以直接发送数据包。

实例:写一个简单的下棋程序,服务端把udp客户端发来的下棋坐标(x,y)信息显示出来,并把(x,y)坐标加1(模拟服务端下棋),再发给客户端。

服务端:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('127.0.0.1', 8888))  # 绑定到8888端口,注意127.0.0.1是特殊地址,如果客户端在另一台电脑,则需要准确的本机ip地址绑定(如192.168.0.X)
print('绑定udp到端口8888...')
while True:
    data, addr = s.recvfrom(1024)
    print('从%s:%s接受数据' % addr)
    print('已接收', data)
    p = data.decode('utf-8').split(',')  # 将接收的字符串解码
    x = int(p[0])
    y = int(p[1])
    print(p[0], p[1])
    pos = str(x + 1) + ',' + str(y + 1)  # 模拟服务端下棋的位置
    s.sendto(pos.encode('utf-8'), addr)  # 发回客户端

客户端:

import socket

s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)

x=input('输入x坐标')
y=input('输入y坐标')
data=str(x)+','+str(y)
s.sendto(data.encode('utf-8'),('127.0.0.1',8888))#将字符串转为传送的字符串,如客户端不在本机,则需要服务器准确的地址
data2,addr=s.recvfrom(1024) #接收服务器返回的数据
print('接收服务器加1后坐标数据:',data2.decode('utf-8'))
s.close()

recvfrom()方法接收数据和客户端的地址和端口,以及具体数据

sendto()方法可以通过udp发送数据

 

 

以下为实例代码:

服务器端:

# -*- coding: utf-8 -*-
# @Time : 2020/11/29 17:22
# @Author : Zhenghui Lyu
# @FileName: wuziqiserver.py
# @Email : lzhfootmark@163.com
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/ZhenghuiLyu


from tkinter import *
from tkinter.messagebox import *
import socket
import threading
import os


def drawQiPan():  # 绘制15*15的棋盘
    for i in range(0, 15):
        cv.create_line(20, 20 + 40 * i, 580, 20 + 40 * i, width=2)
    for i in range(0, 15):
        cv.create_line(20 + 40 * i, 20, 20 + 40 * i, 580, width=2)
    cv.pack()


def checkWin(x, y):
    '''检查是否胜利,检查当前坐标相连情况'''
    flag = False  # 胜利标志

    color = map[x][y]
    # print('color值为:', color)

    i = 1
    count = 1  # 统计相连棋子
    while color == map[x + i][y]:  # 右横向连接相同
        count += 1
        i += 1
    i = 1
    while color == map[x - i][y]:  # 左横向连接相同
        count += 1
        i += 1
    if count >= 5:
        flag = True  # 连5则胜利

    i2 = 1
    count2 = 1
    while color == map[x][y + i2]:  # 统计纵向上连接
        count2 = count2 + 1
        i2 = i2 + 1
    i2 = 1
    while color == map[x][y - i2]:  # 统计纵向下连接
        count2 = count2 + 1
        i2 = i2 + 1
    if count2 >= 5:
        flag = True

    i3 = 1
    count3 = 1
    while color == map[x + i3][y - i3]:  # 右上,左下连接
        count3 += 1
        i3 += 1
    i3 = 1
    while color == map[x - i3][y + i3]:
        count3 += 1
        i3 += 1
    if count >= 5:
        flag = True

    i4 = 1
    count4 = 1
    while color == map[x + i4][y + i4]:  # 右下,左上连接
        count4 = count4 + 1
        i4 += 1
    i4 = 1
    while color == map[x - i4][y - i4]:
        count4 += 1
        i4 += 1
    if count4 >= 5:
        flag = True
    return flag


def sendMessage(pos):
    global s
    global addr
    s.sendto(pos.encode(), addr)  # 按照协议将字符串发出


def callpos(event):
    global turn
    global Myturn  # 服务器端下白棋
    if Myturn == -1:
        Myturn = turn
    else:
        if (Myturn != turn):
            showinfo(title='提示', message='还没轮到自己走棋')
            return
    x = (event.x) // 40  # 换算棋盘坐标
    y = (event.y) // 40
    print("点击在", x, ',', y)
    if map[x][y] != " ":
        showinfo(title='提示', message='wrong,已有棋子')
    else:
        img1 = imgs[turn]
        cv.create_image((x * 40 + 20, y * 40 + 20), image=img1)
        cv.pack()
        map[x][y] = str(turn)
        pos = str(x) + ',' + str(y)
        sendMessage('move|' + pos)
        print('服务器走棋的位置', pos)
        label1['text'] = "服务器走棋的位置" + pos
    if checkWin(x, y):
        if turn == 0:
            showinfo(title='提示', message='黑方赢了')
            sendMessage('over|黑方赢了')
        else:
            showinfo(title='提示', message='白方赢了')
            sendMessage('over|白方赢了')
    if turn == 0:
        turn = 1
    else:
        turn = 0
    print('Myturn值为:', Myturn)
    print('turn值为:', turn)


def callexit(event):
    pos = '退出!'
    sendMessage(pos)
    os._exit(0)


def drawOtherChess(x, y):
    global turn
    img1 = imgs[turn]
    cv.create_image((x * 40 + 20, y * 40 + 20), image=img1)
    cv.pack()  # 绘制图形
    map[x][y] = str(turn)  # 0,1组成期盼逻辑
    if turn == 0:
        turn = 1
    else:
        turn = 0


def receiveMessage():
    global s
    while True:
        global addr
        data, addr = s.recvfrom(1024)  # 从发送方接收数据,接收到的数据data为字符串类型,保存在data中,从对方发送过来的地址保存在addr中。1024表示一次最多接收1024个字节。

        data = data.decode('utf-8')
        print(data)
        a = data.split('|')  # 分隔数据为列表,|前面可能是move/over/join/exit等
        if not data:
            print("客户端退出")
            break
        elif a[0] == 'join':
            print("客户端连接至服务器")
            label1['text'] = '连接服务器成功,请你走棋!'
        elif a[0] == 'exit':
            print('客户端退出,游戏结束!')
        elif a[0] == 'over':
            print("对方胜利!")
            label1['text'] = data.split('|')[0]
            showinfo(title='提示', message=data.split('|')[1])
        elif a[0] == 'move':
            print("接收到", data, '来自', addr)
            p = a[1].split(',')  # 客户端走的信息,如move|7,4 ,则在(7,4)坐标下棋
            x = int(p[0])
            y = int(p[1])
            print(p[0], p[1])
            label1['text'] = "客户端走的位置是" + p[0] + ',' + p[1]
            drawOtherChess(x, y)  # 调用画棋函数
    s.close()


def startNewThread():
    thread = threading.Thread(target=receiveMessage, args=())
    thread.setDaemon(True)
    thread.start()


root = Tk()
root.title('网络五子棋v2.0--服务器端')
imgs = [PhotoImage(file='blackstone.gif'), PhotoImage(file='whitestone.gif')]
turn = 0  # 0为黑方,1为白
Myturn = -1
map = [[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] for y in range(15)]
cv = Canvas(root, bg='green', width=610, height=610)
drawQiPan()  # 画出棋盘
cv.bind('<Button-1>', callpos)  # 点击引发下棋事件
cv.pack()
label1 = Label(root, text='服务器端...等待连接')
label1.pack()
button1 = Button(root, text='退出游戏')
button1.bind('<Button-1>', callexit)
button1.pack()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('192.168.0.11', 8000))
addr = ('localhost', 8000)
startNewThread()
root.mainloop()

客户端:

# -*- coding: utf-8 -*-
# @Time : 2020/11/30 16:05
# @Author : Zhenghui Lyu
# @FileName: wuziqiclient.py.py
# @Email : lzhfootmark@163.com
# @Software: PyCharm
# @Blog :https://www.cnblogs.com/ZhenghuiLyu

from tkinter import *
from tkinter.messagebox import *
import socket
import threading
import os


def drawQiPan():  # 绘制15*15的棋盘
    for i in range(0, 15):
        cv.create_line(20, 20 + 40 * i, 580, 20 + 40 * i, width=2)
    for i in range(0, 15):
        cv.create_line(20 + 40 * i, 20, 20 + 40 * i, 580, width=2)
    cv.pack()


def checkWin(x, y):
    '''检查是否胜利,检查当前坐标相连情况'''
    flag = False  # 胜利标志

    color = map[x][y]

    i = 1
    count = 1  # 统计相连棋子
    while color == map[x + i][y]:  # 右横向连接相同
        count += 1
        i += 1
    i = 1
    while color == map[x - i][y]:  # 左横向连接相同
        count += 1
        i += 1
    if count >= 5:
        flag = True  # 连5则胜利

    i2 = 1
    count2 = 1
    while color == map[x][y + i2]:  # 统计纵向上连接
        count2 = count2 + 1
        i2 = i2 + 1
    i2 = 1
    while color == map[x][y - i2]:  # 统计纵向下连接
        count2 = count2 + 1
        i2 = i2 + 1
    if count2 >= 5:
        flag = True

    i3 = 1
    count3 = 1
    while color == map[x + i3][y - i3]:  # 右上,左下连接
        count3 += 1
        i3 += 1
    i3 = 1
    while color == map[x - i3][y + i3]:
        count3 += 1
        i3 += 1
    if count >= 5:
        flag = True

    i4 = 1
    count4 = 1
    while color == map[x + i4][y + i4]:  # 右下,左上连接
        count4 = count4 + 1
        i4 += 1
    i4 = 1
    while color == map[x - i4][y - i4]:
        count4 += 1
        i4 += 1
    if count4 >= 5:
        flag = True
    return flag


def sendMessage(pos):
    global s
    # global addr
    # s.sendto(pos.encode(), addr)  # 按照协议将字符串发出
    s.sendto(pos.encode(), (host, port))


def callback(event):
    global turn
    global Myturn
    if Myturn == -1:
        Myturn = turn
    else:
        if Myturn != turn:
            showinfo(title='提示', message='还没轮到自己走棋')
            return
    x = (event.x) // 40  # 换算棋盘坐标
    y = (event.y) // 40
    print("点击在", x,',', y)
    if map[x][y] != " ":
        showinfo(title='提示', message='已有棋子')
    else:
        img1 = imgs[turn]
        cv.create_image((x * 40 + 20, y * 40 + 20), image=img1)
        cv.pack()
        map[x][y] = str(turn)
        pos = str(x) + ',' + str(y)
        sendMessage('move|' + pos)
        print('客户端走棋的位置' + pos)
        label1['text'] = "客户端走棋的位置" + pos
    if checkWin(x, y):
        if turn == 0:
            showinfo(title='提示', message='黑方你赢了')
            sendMessage('over|黑方赢了')
        else:
            showinfo(title='提示', message='白方你赢了')
            sendMessage('over|白方赢了')
    if turn == 0:
        turn = 1
    else:
        turn = 0


def callexit(event):
    pos = '退出!'
    sendMessage(pos)
    os._exit(0)


def drawOtherChess(x, y):
    global turn
    img1 = imgs[turn]
    cv.create_image((x * 40 + 20, y * 40 + 20), image=img1)
    cv.pack()  # 绘制图形
    map[x][y] = str(turn)  # 0,1组成期盼逻辑
    if turn == 0:
        turn = 1
    else:
        turn = 0


def receiveMessage():
    global s
    while True:
        # global addr
        # data, addr = s.recvfrom(1024)
        data = s.recv(1024).decode('utf-8')
        a = data.split('|')  # 分隔数据为列表,|前面可能是move/over/join/exit等
        if not data:
            print("服务器退出")
            break
        # elif a[0] == 'join':
        #     print("客户端连接至服务器")
        #     label1['text'] = '连接服务器成功,请你走棋!'
        elif a[0] == 'exit':
            print('对方退出,游戏结束!')
        elif a[0] == 'over':
            print("对方胜利!")
            label1['text'] = data.split('|')[0]
            showinfo(title='提示', message=data.split('|')[1])
        elif a[0] == 'move':
            print("接收到", data)
            p = a[1].split(',')
            x = int(p[0])
            y = int(p[1])
            print(p[0], p[1])
            label1['text'] = "服务器走的位置是" + p[0] +','+ p[1]
            drawOtherChess(x, y)  # 调用画棋函数
    s.close()


def startNewThread():
    thread = threading.Thread(target=receiveMessage, args=())
    thread.setDaemon(True)
    thread.start()


root = Tk()
root.title('网络五子棋v2.0--客户端')
imgs = [PhotoImage(file='blackstone.gif'), PhotoImage(file='whitestone.gif')]
turn = 0  # 0为黑方,1为白
Myturn = -1
map = [[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] for y in range(15)]
cv = Canvas(root, bg='green', width=610, height=610)
drawQiPan()
cv.bind('<Button-1>', callback)
cv.pack()
label1 = Label(root, text='客户端...')
label1.pack()
button1 = Button(root, text='退出游戏')
button1.bind("<Button-1>", callexit)
button1.pack()

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
port = 8000
host = '192.168.0.11'
pos = 'join|'
sendMessage(pos)
startNewThread()
root.mainloop()

注意:checkWIn函数有bug,此程序仅作示例用途。