Python、/C/C++ Socket编程实例

Socket:

socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。 
在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。

SOCKET的流程

正如它的直译,它的功能也就像插座一样。(经典的比喻是电话网,这里不再赘述)(括号中是事实上的对象)

对供应商(server)来说:

1. 先装(申请)一个插座(socket)
2. 把插座(socket)登记到电网中的固定位置(bind)
3. 坐等用户(client)连接(listen)
4. 接受用户的连接申请(accept)
5. 进行数据交换(read&write)
6. 关闭连接(close)

这里面要特殊说明的一点是accept部分,服务器accept以后并不是直接在当前插座上与用户交流,而是会返回一个 新socket ,这个socket和用户相连。可以想象用户的线头刚刚从插座里钻出来,就被listening的server accept:抓到旁边一个新socket里面,从而 空出 那个对外的socket继续接收别人的connect申请。

而对用户来说:

1. 装一个插座(socket)
2. 连接(connect)供应商(server)
3. 取电付费(read&write)
4. 不用了关掉(close)

如此便完成了不同程序之间( 即不一定非要不同电脑,也可以用于进程之间 )的信息传递。

Python、/C/C++ Socket编程实例_第1张图片


详细流程:

来自:点击打开链接

要注意的是函数失败后linux返回的是-1,windows返回SOCKET_ERROR(等于-1)。windows下用GCC似乎还要加-lwsock32命令。windows下返回的数据类型是SOCKET(然而也只是对u_int的包装而已),linux下返回int。即socket的代号。下文为了区分SOCKET与别的数据类型统一以SOCKET来做套接字的类型。


1.头文件
对于windows:

    #include

然后还需要初始化socket的DLL:

    WSADATA wsaData;
    WSAStartup(0x202,&wsaData);

对于Linux:

    #include      
    #include 
    #include     

2.申请socket

    SOCKET socket(int domain, int type, int protocol);

domain指明所用的协议域,我们常用AF_INET(其实只是一个常数),代表IPv4协议),除此之外还有如代表IPv6的AF_INET6等等。

type则是连接类型,包括之前说的流(SOCKET_STREAM),数据包(SOCKET_DGRAM)以及一些其它方式。这里不多介绍其它方式。

protocol是协议,一般用0,让其自动与type匹配。

3.地址的保存形式

连接之前,我们得先知道要连向哪里。因此我们需要一个保存服务器信息的结构
这里就有两个很有趣的东西了,一个是sockaddr,另一个是sockaddr_in。sockaddr是一个统一通用的地址结构的数据,而sockaddr_in则如其名是对internet特化的格式,同理还有sockaddr_in6,sockaddr_un等特化子集。sockaddr_in加入了最后的一个空字符串,使得两个大小对齐,从而可以直接当作sockaddr用。

Python、/C/C++ Socket编程实例_第2张图片
通常的做法是:填值的时候使用sockaddr_in结构,而作为函数(如bin, accept, connect等)的参数传入的时候转换成sockaddr结构就行了,毕竟都是16个字符长。
      struct   sockaddr_in   {  
                short   int   sin_family;    
                unsigned   short   int   sin_port; 
                struct   in_addr   sin_addr;    
                unsigned   char   sin_zero[8]; 
        };          

其中的in_addr结构保存了IP,由于我们不需要直接操作,里面的内容就留给有兴趣的读者自己查找资料了。

  • sin_family:指代协议族,只能是AF_INET。

有人会奇怪,协议族为什么只能是AF_INET呢?如果我socket那里填AF_INET6怎么办呢?
答案是,这个只是IPv4的地址结构,如果用IPv6的话需要用到sockaddr_in6。

  • sin_port:存储端口号(使用网络字节顺序)。

不同的CPU有不同的字节序类型 这些字节序是指整数在内存中保存的顺序 这个叫做主机序
最常见的有两种

  1. Little endian:将低序字节存储在起始地址
  2. Big endian:将高序字节存储在起始地址

而网络字节顺序则统一为Big endian。因此我们需要用htons(MYPORT)进行转换。

  • sin_addr:存储IP地址。

使用中我们用inet_addr函数来转换。 如:inet_addr("192.168.0.1")
当然,也可以使用INADDR_ANY让其自动指定。

  • sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。


4.绑定

    int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);

前两个参数我们都已经谈过,第二个只需把sockaddr_in的指针强行转换成sockaddr送进去即可。但第三个socklen_t又是什么呢?

其实socklen_t就是int。其值应为sockaddr结构的大小,可设置为sizeof(struct sockaddr)

POSIX开始的时候用的是size_t, Linus Torvalds(他希望有更多的人,但显然不是很多) 努力向他们解释使用size_t是完全错误的,因为在64位结构中 size_t和int的长度是不一样的,而这个参数(也就是accept函数 的第三参数)的长度必须和int一致,因为这是BSD套接字接口 标准.最终POSIX的那帮家伙找到了解决的办法,那就是创造了 一个新的类型"socklen_t".Linux Torvalds说这是由于他们发现了自己的错误但又不好意思向大家伙儿承认,所以另外创造了一个新的数据类型.(联系UNP卷一P25)

5.监听

    int listen(SOCKET sockfd, int backlog)
对于监听socket文件描述符sockfd,内核要维护两个队列,分别为未完成连接队列和已完成连接队列,这两个队列之和不超过backlog


6.连接/接受连接

int connect(SOCKET sockfd, const struct sockaddr *serv_addr,socklen_t addrlen)

连接其实和绑定的参数一模一样。

int accept(SOCKET sockfd, struct sockaddr *addr, socklen_t *addrlen)

除了要注意第三个参数变成了长度的指针以外没有太大变化。

然而这一句背后的过程却是比较丰富的:

Python、/C/C++ Socket编程实例_第3张图片

这其实就是大名鼎鼎的三次握手,从客户端发送请求开始,在握手的过程中,在服务器内部,该连接申请也从SYN队列走到了ACCEPT队列。这时其实已经生成了新的套接字(新的套接字依然运行在原来的端口上)。accept做的只是取出该套接字而已。换句话说,在我listen以后,图右边的内核部分就开始正常运作了,在我accept之前,我要accept的内容可能就已经生成完毕,客户端发送第三条握手并不需要我accept。

7.读写

    ssize_t read(SOCKET sockfd, void *buf ,size_t count)
    ssize_t write (SOCKET sockfd,const void *buf,size_t count)
    ssize_t recv(SOCKET sockfd, void *buf, size_t len, int flags)
    ssize_t send(SOCKET sockfd, const void *buf, size_t len, int flags)

返回实际读写的字节数。第二个参数为字符串的指针,第三个参数为可接受的最大长度。recv和send的第四个为特殊设置,为0时等价于read write。
要注意read和write只能在linux下使用,为了更好的复用性应该统一使用recv和send

    ssize_t sendto(SOCKET sockfd, const void *buf, size_t len, int flags,const struct sockaddr *to, socklen_t tolen)
    ssize_t recvfrom(SOCKET sockfd, void *buf, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen)

这两个是UDP模式的发送/接收函数,用UDP的时候只需申请+绑定即可发送数据。参数也只是前面其它函数参数的结合罢了。

然而正如我们之前看到过的,底层通过SendQ,RecvQ两个队列来分别储存发送了但没有被对方接收到接收到但没有被程序读取的数据。

Python、/C/C++ Socket编程实例_第4张图片

由于网络传输需要时间,很可能当我读的时候,数据的一部分还在路上,另外正如我们刚刚说的,读操作是有个指定大小的参数的,那如果队列中的数据多于它,那就只读取上限值的数据。这两种情况我们都能想象:队列中的数据被“截断”了。(当然这是针对数据发送时被切分了的情况,数据很小时不用考虑)因此人们这样总结:

这正是 SOCK_STREAM 与 SOCK_DGRAM 的区别。前者是流式传输,保证顺序,但是不保证消息边界,也就是说多次发送可能一次接受完,也可能一次发送花了多次接收。TCP 是其典型。后者数据报方式的话,是不可靠的消息传输,保持消息边界的。UDP 是其典型。 


8.断开
windows下可以使用closesocket()
或者#include 后close()(两系统均可).
这两种方式几乎相等,都会使当前套接字的引用计数-1,当减到0时就开始终止连接。
而还有一种shutdown方式,会强制断开。close与shutdown的详细区别这里不多说。
有一点要注意的是,如果其中一方断开了,另一方在read/write的时候会在返回0的同时得到一个SIGPIPE信号。这个信号会将程序终止。因此,如果你需要复数的连接,最好设置把SIGPIPE忽略掉。忽略SIGPIPE的命令如下

    signal(SIGPIPE,SIG_IGN);

至于关闭时候的底层细节由于比较复杂这里不讨论,有兴趣的可以看看:【Java TCP/IP Socket】深入剖析socket--TCP套接字的生命周期
我们只要注意close的时候确保两遍都正常close关闭即可。

推荐阅读:socket原理详解

实践:

C/C++版:
// Server端
#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib")


int __cdecl main(_In_ int argc, _In_reads_(argc) _Pre_z_ char** argv, _In_z_ char** envp)
{
	// 初始化套接字动态库
	WSADATA wsaData;
	int err = WSAStartup(0x202, &wsaData);
	if (err != 0)
	{
		wprintf_s(L"WSAStartup failed with error: %d\n", err);
		return 0;
	}

	// 申请socket
	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 绑定
	sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(9990);
	server_addr.sin_addr.S_un.S_addr = INADDR_ANY;
	if (bind(sock, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR)
	{
		wprintf_s(L"bind failed with error %u\n", WSAGetLastError());
		goto _end;
	}
	// 监听
	if (listen(sock, 5) == SOCKET_ERROR)
	{
		wprintf_s(L"listen failed with error %u\n", WSAGetLastError());
		goto _end;
	}
	printf_s("服务器已启动:监听中...\n");
	// 接受连接
	sockaddr_in client_addr;
	int addrlen = sizeof client_addr;
	SOCKET client_sock = accept(sock, (sockaddr*)&client_addr, &addrlen);// 会阻塞进程,直到有客户端连接上来为止
	if (client_sock == INVALID_SOCKET)
	{
		wprintf_s(L"accept failed with error %u\n", WSAGetLastError());
		goto _end;
	}
	char ip[256];
	inet_ntop(AF_INET, &(client_addr.sin_addr.S_un.S_addr), ip, 16);
	printf_s("接收到一个连接: %s\r\n", ip);
	// 收/发客户端数据
	char txt[256] = {'\0'};
	char buff[256] = {'\0'};
	int times = 0;
	while (times++ < 30)
	{
		// 向客户端发送数据
		sprintf_s(txt, "The %dth time: Hello client, this is server!\r\n", times);
		send(client_sock, txt, strlen(txt), 0);
		Sleep(3000);
		// 从客户端接受数据
		memset(buff, '\0', sizeof(buff) * sizeof(char));
		if (recv(client_sock, buff, 256, 0)//0-将socket对应的内核缓冲区的数据剪切到应用程序缓冲区,MSG_PEEK-将socket对应的内核缓冲区的数据拷贝到应用程序缓冲区
			== SOCKET_ERROR)
			wprintf_s(L"recv failed with error %u\n", WSAGetLastError());
		else
			printf_s("从客户端接收到数据: %s\r\n", buff);
	}
_end:
	if (closesocket(sock) == SOCKET_ERROR)
		wprintf_s(L"closesocket failed with error %u\n", WSAGetLastError());
	WSACleanup();// 释放套接字资源

	system("pause");
	return 0;
}

// Client端
#include 
#include 
#include 
#pragma comment(lib, "ws2_32.lib") 

int __cdecl main(_In_ int argc, _In_reads_(argc) _Pre_z_ char** argv, _In_z_ char** envp)
{
	// 初始化套接字动态库
	WSADATA wsaData;
	int err = WSAStartup(0x202, &wsaData);
	if (err != 0)
	{
		wprintf_s(L"WSAStartup failed with error: %d\n", err);
		return 0;
	}

	// 申请socket
	SOCKET host_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	// 服务器端口
	sockaddr_in server_addr;// 服务端地址
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(9990);
	server_addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
	//inet_pton(AF_INET, "127.0.0.1", &(server_addr.sin_addr.S_un.S_addr));// inet_addr("127.0.0.1");//注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址,如果你的计算机没有联网,直接使用127.0.0.1即可
	//if (bind(host_sock, (sockaddr*)&addr, sizeof(addr) == SOCKET_ERROR))
	//{
	//	wprintf_s(L"bind failed with error %u\n", WSAGetLastError());
	//	goto _end;
	//}
	// 连接
	if (connect(host_sock, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR)
	{
		wprintf_s(L"connect failed with error %u\n", WSAGetLastError());
		goto _end;
	}
	printf_s("Connected to server.\n");
	// 收/发服务端数据
	char txt[256] = {'\0'};
	char buff[256] = {'\0'};
	int times = 0;
	while (times++ < 30)
	{
		// 向服务端发送数据
		sprintf_s(txt, "The %dth time: Hello server, this is client!\r\n", times);
		send(host_sock, txt, strlen(txt), 0);
		Sleep(3000);
		// 从服务端接受数据
		memset(buff, '\0', sizeof(buff)*sizeof(char));
		if (recv(host_sock, buff, 256, 0)//0-将socket对应的内核缓冲区的数据剪切到应用程序缓冲区,MSG_PEEK-将socket对应的内核缓冲区的数据拷贝到应用程序缓冲区
			== SOCKET_ERROR)
			wprintf_s(L"recv failed with error %u\n", WSAGetLastError());
		else
			printf_s("从服务端接收到数据: %s\r\n", buff);
	}
_end:
	if (closesocket(host_sock) == SOCKET_ERROR)
		wprintf_s(L"closesocket failed with error %u\n", WSAGetLastError());
	WSACleanup();// 释放套接字资源

	system("pause");
	return 0;
}

Test...
Python、/C/C++ Socket编程实例_第5张图片
Python版:
// Server端
import socket
import threading
import time

def tcplink(sock, addr):
    print('Accept new connection from %s:%s...' % addr)
    sock.send(b'Welcome!')
    while True:
        data = sock.recv(1024)
        time.sleep(1)
        if not data or data.decode('utf-8') == 'exit':
            break
        sock.send(('Hello, %s!' % data.decode('utf-8')).encode('utf-8'))
    sock.close()
    print('Connection from %s:%s closed.' % addr)

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

# 监听端口
s.bind(('127.0.0.1', 9999))
s.listen(5)
print('Waiting for connection...')

while True:
    # 接手一个新连接:
    sock, addr = s.accept()
    t = threading.Thread(target=tcplink, args=(sock, addr))
    t.start()


// Client端
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 建立连接:
s.connect(('127.0.0.1', 9999))
# 接收欢迎消息:
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()

Test...

Python、/C/C++ Socket编程实例_第6张图片


你可能感兴趣的:(C++,Python,Socket,网络编程)