Socket Programming Learning Handbook
声明
Author:Qftm
Data:2020/03/22
Blog:https://qftm.github.io/
正文
由于最近在梳理网络协议栈这部分内容,所以花了一部分时间把以前所学的Socket
知识重新整理总结了一下,总结这个学习手册也是方便自己以后的查看。
Table of Contents
Socket基础
套接字(Socket)最初是由加利福尼亚大学Berkeley分校为UNIX操作系统开发的网络通信接口,随着UNIX操作系统广泛使用,套接字成为当前最流行的网络通信应用程序接口之一。
Windows Sockets API是微软 Windows的网络程序设计接口,它在继承了Berkeley Sockets主要特征的基础上,又对它进行了重要扩充。
套接字
socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口。将复杂的TCP/IP协议族隐藏在 socket接口的背后,通过调用简单的socket函数完成特定协议的数据传输。
- 流式套接字(SOCK_STREAM)
面向连接的、可靠的数据传输服务,在传输层使用 TCP协议。
- 数据报套接字(SOCK_DGRAM)
提供无连接服务,在传输层使用UDP协议。
- 原始套接字(SOCK_RAW)
允许对底层协议进行访问。用于实现自己定制协议或者对数据报做底层的控制。
socket 的原意是“插座”,在计算机通信领域,socket 被翻译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定,一台计算机可以接收其他计算机的数据,也可以向其他计算机发送数据。
socket 的典型应用就是 Web 服务器和浏览器:浏览器获取用户输入的 URL,向服务器发起请求,服务器分析接收到的 URL,将对应的网页内容返回给浏览器,浏览器再经过解析和渲染,就将文字、图片、视频等元素呈现给用户。
主机间通信
在早期的单机系统中,各进程都是运行在自己的地址空间里面,进程之间互不干扰。操作系统为了解决进程之间可以相互通信并且又不互相干扰,为进程之间提供了多种通信机制,比如说管道、命名管道、消息队列、共享内存和信号量等。但是这些机制都仅限于本机的进程通信,如果要解决网络间的进程通信怎么做呢?
在单机上面,两个进程之间只要知道进程id就好办,但是在网络间知道进程的id是没有用的。举个例子,必须要让对方进程知道,我(进程)现在在哪个地址(ip)、哪个地方见面(端口)以及见面要遵循哪样的方式(协议)。也就是说网络间的通信离不开ip、协议、端口这三个要素,这三要素就可以标识网络的一个进程了。而这个协议现在用的最多的就是tcp/ip协议了,使用tcp/ip协议的应用程序通常采用的编程接口有socket和tci来实现进程的通信,就目前而言,tci已经被淘汰了,所以几乎所有的应用程序都是采用socket来编程了。
socket可以看成是用户进程与内核网络协议栈的编程接口。socket不仅可以用于单机进程之间的通信,也可以用在网络进程之间的通信。socket作为一个编程接口在网络中所处的位置如下图:
UNIX/Linux
在Linux中,一切都是文件,除了文本文件、源文件、二进制文件等,一个硬件设备也可以被映射为一个虚拟的文件,称为设备文件。例如,stdin 称为标准输入文件,它对应的硬件设备一般是键盘,stdout 称为标准输出文件,它对应的硬件设备一般是显示器。对于所有的文件,都可以使用 read() 函数读取数据,使用 write() 函数写入数据。
在 UNIX/Linux 系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。
为了表示和区分已经打开的文件,UNIX/Linux 会给每个文件分配一个 ID,这个 ID 就是一个整数,被称为文件描述符(File Descriptor)。例如:
- 通常用 0 来表示标准输入文件(stdin),它对应的硬件设备就是键盘;
- 通常用 1 来表示标准输出文件(stdout),它对应的硬件设备就是显示器。
- 通常用 2 来表示标准错误(stderr)。
UNIX/Linux 程序在执行任何形式的 I/O 操作时,都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。
注意,网络连接也是一个文件,它也有文件描述符!
在Linux中,socket 也被认为是文件的一种,和普通文件的操作没有区别,所以在网络数据传输过程中自然可以使用与文件 I/O 相关的函数。可以认为,两台计算机之间的通信,实际上是两个 socket 文件的相互读写。
我们可以通过 socket() 函数来创建一个网络连接,或者说打开一个网络文件,socket() 的返回值就是文件描述符。有了文件描述符,我们就可以使用普通的文件操作函数来传输数据了,例如:
- 用 read() 读取从远程计算机传来的数据;
- 用 write() 向远程计算机写入数据。
Window
Windows 也有类似“文件描述符”的概念,但通常被称为“文件句柄”。与 UNIX/Linux 不同的是,Windows 会区分 socket 和文件,Windows 就把 socket 当做一个网络连接来对待,因此需要调用专门针对 socket 而设计的数据传输函数,针对普通文件的输入输出函数就无效了。
Socket API 编程模型
SOCK_STREAM协议域(TCP)模型
socket之间的连接可以分为三个步骤:服务端监听、客户端请求、连接确认,从上面的模型中我们可以看到,通信主要是依靠“write-read / read-write ”这种模型来进行数据的读写。
数据传输方式
计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。
(1)SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
(2)SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
下面介绍Socket API的编程模型会以SOCK_STREAM(TCP)方式去讲述,环境为Linux下socket编程。
TCP协议
TCP协议是一种面向连接、面向字节流的可靠传输层协议。两台采用TCP协议通信的计算机首先要建立TCP连接。TCP协议以它自己的方式缓存数据,缓存过程对程序员和用户是透明的。TCP协议采用“捎带确认(piggybacking ACK)”的方法,允许双方同时发送数据。TCP规定了报文段的最大报文段长度(MSS),默认的MSS值为536个字节。
TCP报头的固定长度是20个字节,如果使用一些选项,TCP报头的最大长度为60个字节。TCP报头的结构如下图所示。
TCP报头的各字段依次为:
(1)源端口: 16位,本地TCP端口号。
(2)目的地端口:16位,目的TCP端口号。
(3)序号:32位,用来跟踪发送报文字节顺序的序号。序号随着通信的进行不断的递增,当达到最大值的时候重新回到0再开始递增。TCP是面向字节流的,在一个TCP连接中传送的字节流中的每一个字节都按照顺序编号。整个要传送的字节流的起始号必须在连接建立 时设置。下个序列号(发送)等于上个序列号(接 收)加上报文长度。
(4)确认号:32位,用于确认对上个数据包接收成功。 确认号(发送)等于上个序列号(接收)加一 。
(5)数据偏移:4位,表示以4个字节为单位的报头长度。指出TCP报头从起始端到数据端的距离,该字段描述了TCP报头的长度。 由于option字段的存在,所以TCP报头的长度往往是 不确定的,因此该字段很有存在的必要了。需要注意的是“数据偏移”计算的单位是32位(即4个字节为一个计算单位)。因此“数据偏移”有4个位所以能够表达的最大的十进制为15,也就说TCP报头的最 大长度为60字节。
(6)保留:6位,保留为今后使用,目前该字段为全 0。
(7)标志位:6位,用于标志数据包。
URG:该字段为1时紧急发送数据。相当于提高数据发送的优先级,不按照原来队列顺序来进行发送,同时启用紧急指针。
ACK:该字段为1时表示确认号有效,当该位为0是表示确认号无效。TCP规定,建立链接后所有数据报文段ACK都设为1。
PSH:该字段为1时紧急接收数据。该字段允许数据包不需要等到接收端的缓存(窗口)满了后才上交数据,而是直接上交数据。
RST:该字段为1时表示该连接出现严重的错误,必须释放该连接再重新建立连接进行数据传输。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。
SYN:该字段位1时表示发送连接请求,用来在建立连接时进行同步序号。
FIN:该字段为1时表示发送释放请求,用于释放当前的连接。
(8)窗口:占16位。窗口指的是接收窗口。用于限制发送方当前允许发送的数据量。这是因为接收方的数据缓存空间是有限的。
(9)检验和:占16位。检验和字段检验的范围包括首部和数据两部分。
(10)紧急指针:占16位。当URG=1的时候才生效,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此紧急指针指出了紧急数据的末尾报文段中的位置。当所有的紧急数据都处理完毕时,TCP就告诉应用程序恢复正常的操作。
(11)选项:长度可变,最长可达40字节(320位)。当没有使用option字段的时候TCP报文首部长度为20字节。
(12)填充:为了使整个首部长度是 4 字节的整数倍。
TCP三次握手
所谓三次握手(Three-Way Handshake)即建立TCP连接,就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
TCP三次握手:
(1)客户端主动打开,发送连接请求报文段,将SYN标识位置为1,Sequence Number置为x(TCP规定SYN=1时不能携带数据,x为随机产生的一个值),然后进入SYN_SEND状态
(2)服务器收到SYN报文段进行确认,将SYN标识位置为1,ACK置为1,Sequence Number置为y,Acknowledgment Number置为x+1,然后进入SYN_RECV状态,这个状态被称为半连接状态
(3)客户端再进行一次确认,将ACK置为1(此时不用SYN),Sequence Number置为x+1,Acknowledgment Number置为y+1发向服务器,最后客户端与服务器都进入ESTABLISHED状态
SYN攻击:
在三次握手过程中,Server发送SYN-ACK之后,收到Client的ACK之前的TCP连接称为半连接(half-open connect),此时Server处于SYN_RCVD状态,当收到ACK后,Server转入ESTABLISHED状态。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server回复确认包,并等待Client的确认,由于源地址是不存在的,因此,Server需要不断重发直至超时,这些伪造的SYN包将产时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络堵塞甚至系统瘫痪。SYN攻击时一种典型的DDOS攻击,检测SYN攻击的方式非常简单,即当Server上有大量半连接状态且源IP地址是随机的,则可以断定遭到SYN攻击了,使用如下命令可以让之现行:
netstat -nap | grep SYN_RECV
TCP服务端API
socket建立
socket()
使用man socket
命令查看socket函数的使用方式。函数原型如下
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket() creates an endpoint for communication and returns a descriptor.The domain argument specifies a communication domain; this selects theprotocol family which will be used for communication.
官方的描述:socket()函数创建了一个通信的切入点,并且返回一个描述符,而domain参数制定了一个协议域,指定了哪种协议族被用来作为通信的协议,而type则制定了通信语义,也就是socket的类型,protocol这是指定使用哪种协议。下面分别介绍。
domain 指定协议簇
type 指定套接字的通信类型
protocol 指定使用的协议
(1)domain:常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。由于现在基本上使用32位的ipv4地址,所以在 socket编程中domain一般设置为AF_INET,表明要用ipv4和端口号的组合协议族。
(2)type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
- SOCK_STREAM
流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用,可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收。
- SOCK_DGRAM
数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用,不提供无错保证,数据可能接丢失,不能保证按顺序接收。
- SOCK_RAW
SOCK_RAW工作在网络层,可以处理ICMP、IGMP等网络报文、特殊的IPv4报文、可以通过IP_HDRINCL套接字选项由用户构造IP头。
(3)protocol:指定协议。
对于socket的第一个参数,因为AF_INET、PF_INET、AF_PACKET和PF_PACKET中AF和PF基本等价,所以只需要区分_NET
和_PACKET
就行;使用AF(PF)_INET
,用户程序无法获得链路层数据,也即,以太网头部。简单来说,使用AF(PF)_INET
,是面向IP层的原始套接字;使用AF(PF)_PACKET
,是面向链路层的套接字。
如果使用AF_INET时,我们所构造的报文从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段会被设置为我们调用socket()函数时传递给它的protocol字段。也即,如果没有开启IP_HDRINCL选项,那么内核会帮忙处理IP头部。如果设置了IP_HDRINCL选项,那么用户需要自己生成IP头部的数据,其中IP首部中的标识字段和校验和字段总是内核自己维护。可以通过下面代码开启IP_HDRINCL:
const int on = 1;
if(setsockopt(fd,SOL_IP,IP_HDRINCL,&on,sizeof(int)) < 0)
{
printf("set socket option error!\n");
}
如果第一个参数是AF(PF)_INET
,那么是面向IP层的套接字,protocol字段定义在/usr/include/netinet/in.h
中,常见的包括IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP和IPPROTO_RAW
。前三个参数顾名思义,最后一个IPPROTO_RAW
的含义需要额外说明一下:
使用原始套接字编写底层网络程序时,最重要的决策之一是应用程序是否与传输级协议头,IP头一起构建。现在,告诉IP层不要预先添加自己的头的明显方法是调用setsockopt系统调用并设置IP_HDRINCL(包含头)选项。但是,并不总是存在此选项。在Net/3之前的版本中,没有IP_HDRINCL选项,并且没有内核预先添加自己的头的唯一方法是使用特定的内核补丁并将协议设置为IPPROTO_RAW。这些补丁最初是为4.3BSD和Net / 1制作的,以支持需要编写自己的完整IP数据报的Traceroute,因为它需要修改TTL字段。有趣的是,自从IP_HDRINCL出现以来,Linux和FreeBSD选择了不同的方式来继续“传统”。在Linux上将协议设置为PPROTO_RAW时,默认情况下,内核会设置IP_HDRINCL选项,因此不会在其前面加上自己的IP头。
如果第一个参数是AF(PF)_PACKET
,那么是面向链路层的套接字,protocol字段定义在/usr/include/netinet/if_ether.h
中,第三个参数可以是
- ETH_P_IP - 只接收目的mac是本机的IP类型数据帧
- ETH_P_ARP - 只接收目的mac是本机的ARP类型数据帧
- ETH_P_RARP - 只接收目的mac是本机的RARP类型数据帧
- ETH_P_PAE - 只接收目的mac是本机的802.1x类型的数据帧
- ETH_P_ALL - 接收目的mac是本机的所有类型数据帧,同时还可以接收本机发出的所有数据帧,混杂模式打开时,还可以接收到目的mac不是本机的数据帧
这样创建了一个socket之后,仅仅只获取了这个socket的描述符。但是此时还并没有一个具体的地址,如果要指定一个地址,接下来就要调用bind函数,否则在调用下面的listen函数时候,系统会自动分配一个地址。
socket绑定
bind()
bind函数负责把一个协议族的特定地址赋给socket(成功返回0,失败返回-1,并设置errno变量。),例如AF_INET代表ipv4的地址和端口号的组合、AF_INET6代表ipv6的地址和端口号的组合。函数原型如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
(1)sockfd参数:代表socket的套接字描述符,这个描述符就能代表一个socket,每个进程空间都有一个套接字描述符表,该表在那个存放着套接字描述符和套接字数据接口对应的关系,该表中有一个字段存放套接字的描述符,另外一个字段存放着套接字数据接口地址,因此根据套接字描述符就可以找到对应的套接字数据接口。
(2)addr参数:一个const struct sockaddr *指针,指定要绑定的协议族的地址是多少,协议族不同,这个地址的结构不同。
ipv4的协议族地址结构如下
struct sockaddr_in {
sa_family_t sin_family; // 协议族 比如所AF_INET
in_port_t sin_port; // 网络字节序中的端口
struct in_addr sin_addr; // 网络地址
};
struct in_addr {
uint32_t s_addr; // 网络字节序中的地址
};
(3)addrlen参数:地址的长度
(4)网络字节序:是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,而主机字节序是和具体的主机平台有关联的,因此网络字节序可以保证数据在不同主机之间传输时能够被正解析,因此在使用的时候,协议族中的地址一定要从主机字节序转化成网络字节序。主机字节序就是我们平时说的大端和小端模式,不同的cpu有不同的字节序类型,这些字节序在内存中有不同的保存顺序。
CPU向内存保存数据的方式有两种:
大端序(Big Endian):高位字节存放到低位地址(高位字节在前)。
小端序(Little Endian):高位字节存放到高位地址(低位字节在前)。
不同CPU保存和解析数据的方式不同(主流的Intel系列CPU为小端序),小端序系统和大端序系统通信时会发生数据解析错误。因此在发送数据前,要将数据转换为统一的格式——网络字节序(Network Byte Order)。网络字节序统一为大端序。
主机A先把数据转换成大端序再进行网络传输,主机B收到数据后先转换为自己的格式再解析。
编写C代码检测自己主机的字节序
#include<stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void main()
{
unsigned int data = 0x12345678;
unsigned char *p = (unsigned char *)&data; //只取一个字节
if(p[0] == 0x78){ //*p==0x78
printf("Little-Endian\n");
}else if(p[0] == 0x12){
printf("Big-Endian\n");
}
}
当我们知道主机的字节序类型之后,如何把主机字节序转化成网络字节序呢?这个时候可以参考我们的下列转化函数:
htons 把unsigned short类型从主机序转换到网络序
htonl 把unsigned long类型从主机序转换到网络序
ntohs 把unsigned short类型从网络序转换到主机序
ntohl 把unsigned long类型从网络序转换到主机序
另外需要说明的是,sockaddr_in 中保存IP地址的成员为32位整数。在tcp/ip协议中,ipv4的网络地址是32位的,而我们编程中通常指定的ip地址是点分形式的,如:192.168.1.1,它是一个字符串。如何在点分ip和网络地址之间相互转换呢?
inet_addr() 函数可以完成这种转换。inet_addr() 除了将字符串转换为32位整数,同时还进行网络字节序转换。
char *myIp = "192.168.1.1";
inet_addr(&myIp);
socket监听
listen()
实现服务器等待客户端请求的功能;成功返回0,失败返回-1,并设置errno变量。
在指定了协议族的地址之后,就需要监听这个地址的客户端请求。
为了能够在套接字上接受进入的连接,服务器必须创建一个队列来保存未处理的请求,它用listen系统调用来完成
#include <sys/socket.h>
int listen(int sockfd, int backlog)
(1)sockfd参数:代表的就是刚才所创建的socket的描述符,这里的的socket是一个被动的套接字,其只能用来监听,不能用来做其他任何的操作。
(2)backlog参数:定义了正在排队等待连接的socket描述符的最大个数,SOMAXCONN 表示 128。
Linux系统可能会对队列中可以容纳的未处理连接的最大数目做出限制,为遵守该限制,listen函数将队列长度设置为backlog参数值。
超出backlog值的连接请求会被拒绝
该机制允许服务器忙时将后续backlog以内的客户请求放入队列,默认为5
上述几步操作之后,就可以接收客户端的请求了。
socket接收
accept()
该函数处于监听状态服务器,在获得客户机连接请求后,会将其放置在等待队列中,当系统空闲时,接收客户机的连接请求。没有客户机请求时,阻塞等待。 函数调用成功后,返回最后的服务器端文件描述符, 失败返回-1,设置errno变量。
用于接收客户端的请求,建立两者之间的连接。
一旦服务器程序创建并命名了套接字后,就可以通过accept系统调用来等待客户建立对该套接字的连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
(1)sockfd是刚才创建的被动套接字的描述符。
(2)addr代表的是客户端的协议地址。
(3)协议地址的长度。
如果accept接受请求成功,那么会返回一个全新的socket,代表和客户端的一个tcp连接,并且是一个主动套接字,可以与客户端进行通信读写操作。
如果套接字队列中没有未处理的连接,accept将阻塞(等待,因此返回值将可作为判别执行结果的依据)直到有客户建立连接为止,可以通过为套接字文件描述符设置O_NONBLOCK (不等待,立即返回,因此不可简单将返回值作为判别执行结果的依据)来改变这一行为,使用函数fcntl。
int flags = fcntl(socket, F_GETFL, 0)
fcntl ( socket , F_SETFL, O_NONBLOCK| flags);
TCP客户端API
socket建立
socket()
使用man socket
命令查看socket函数的使用方式。函数原型如下
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
socket() creates an endpoint for communication and returns a descriptor.The domain argument specifies a communication domain; this selects theprotocol family which will be used for communication.
官方的描述:socket()函数创建了一个通信的切入点,并且返回一个描述符,而domain参数制定了一个协议域,指定了哪种协议族被用来作为通信的协议,而type则制定了通信语义,也就是socket的类型,protocol这是指定使用哪种协议。下面分别介绍。
domain 指定协议簇
type 指定套接字的通信类型
protocol 指定使用的协议
socket连接
connect()
用于客户端向服务器发送连接请求。 成功后返回0,失败返回-1,设置errno变量。
在服务器端调用 bind和listent函数后,服务器就可以接受客户端的请求了。此时客户端只需要调用connect函数与服务端调用accept函数进行连接,连接成功后,就可以与服务端进行正常的读写操作。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
connect成功后,服务端的accept函数就会接受到请求,这个时候客户端就可以通过write写数据,服务器端就可以通过read读数据。
socket通信
read/write()
当服务端经过listen,客户端经过connect后,此时两者就可以利用i/o进程通信了,网络io操作可以利用一下几组函数来实现:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
函数原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
size_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
- read()
read函数是负责从fd中读取内容。当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题
- write()
write函数将buf中的nbytes字节内容写入文件描述符fd。成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有两种可能。
write的返回值大于0,表示写了部分或者是全部的数据。
返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。
- send()
将信息发送到指定的套接字文件描述符中。 成功后返回实际发送的字节数,失败返回-1,设置 errno变量。
s:要发送信息的文件描述符。
buf:指向要发送内容的指针。
len:要发送数据的长度。
flags:为0时,与write功能相同
- recv()
从指定的套接字中获取信息。 成功后返回0,失败返回-1,设置errno变量。
s:要读取内容的套接字文件描述符。
buf:指向要保存数据缓冲区的指针。
len:该缓存的最大长度。
flags:和send函数一致。
socket关闭
close()
通过调用close函数终止服务器和客户端上的套接字连接。当客户端与服务端不再需要通信时候,可以调用close函数断开连接。
int close(int fd);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
TCP四次握手
所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:
TCP四次握手:
(1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
(2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
(3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
(4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
(5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
(6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
Linux Socket编程
编写Linxu Socket在不同协议域下通信的代码,server.c 是服务器端代码,client.c 是客户端代码,要实现的功能是:客户端和服务端能够相互通信。
Socket TCP
TCP中,套接字是一对一的关系。
服务端
Serve.c
编写服务端程序代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
void main(){
int domain = AF_INET;
int type = SOCK_STREAM;
int protocol = 0;
int socketFd = socket(domain,type,protocol); //创建socket
if(socketFd != -1){ //判断socket是否创建成功
printf("Server-> socket create success+++++++\n\n");
struct sockaddr_in serveaddr;
serveaddr.sin_family = AF_INET;
serveaddr.sin_port = htons(8888); //port in network byte order
serveaddr.sin_addr.s_addr = inet_addr("192.33.6.145"); //internet address
//const struct sockaddr addr = (struct sockaddr *)&serveaddr 标准化(sockaddr程序员不能直接操作)
socklen_t addrlen = sizeof(serveaddr);
int result = bind(socketFd,(struct sockaddr*)&serveaddr,addrlen);//socket绑定(命名)
if(result == 0){
printf("++++++++++++++被动socket模式++++++++++++++++\n\n");
printf("Serve-> socket1 bind success++++++++++++\n\n");
// 监听的时候的sockfd是一个被动套接字,被动套接字是用来接受连接的,只能用来用来监听
result = listen(socketFd,20); //socket监听来自外界对服务器的请求(和accept函数结合建立TCP三次握手)
if(result == 0){
printf("Serve-> socket1 listening+++++++++++\n\n");
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//accept,会创建一个新的连接,这是一个主动套接字,可以读写,与客户端会话
// 所以服务器端至少有两个套接字
struct sockaddr_in clientaddr;
socklen_t addrlen = sizeof(clientaddr);
unsigned int conn = accept(socketFd,(struct sockaddr*)&clientaddr,&addrlen);
if(conn > 0){
printf("+++++++++++++主动socket模式+++++++++++++\n\n");
printf("Serve-> socket2 accept success+++++++++\n\n");
printf("------------Wait Client Request------------\n\n");
char recv_buf[1024] = {0};
char send_buf[1024] = {0};
while(1){
// 读取客户端发送过来的数据 coon主动套接字
// ssize_t read()(int fd, void *buf, size_t count);
result = read(conn,recv_buf,sizeof(recv_buf));
if(result == 0){
//printf("Serve-> Read end of file!!!\n\n");
}
else if(result == -1){
printf("Serve-> Read error!!!\n\n");
}else{
printf("Client-> ");
fputs(recv_buf,stdout);
printf("\n");
//printf("Serve-> Read success+++++++++\n\n");
// 向客户端回数据
// ssize_t write(int fd, const void *buf, size_t count);
printf("Serve-> ");
fgets(send_buf,sizeof(send_buf),stdin);
printf("\n");
result = write(conn,send_buf,strlen(send_buf));
if(result == 0){
printf("Serve-> Nothing was writen to client!!!\n\n");
}
else if(result == -1){
printf("Serve-> Write to client error!!!\n\n");
}else{
//printf("Serve-> Write to client success+++++++++\n\n");
printf("------------Wait Client Request------------\n\n");
}
}
}
close(conn);
close(socketFd);
}
else{
printf("Serve-> accept fail!!\n\n");
}
}
else{
printf("Serve-> socket listening fail!!\n\n");
}
}
else{
printf("Serve-> socket bind fail!!\n\n");
}
}
else{
printf("Serve-> socket create fail!!\n\n");
}
}
编译写好的server.c程序代码
gcc serve.c -o serve -w
客户端
Client.c
编写客户端程序代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
void main(){
int domain = AF_INET;
int type = SOCK_STREAM;
int protocol = 0;
int socketFd = socket(domain,type,protocol); //创建socket
if(socketFd != -1){ //判断socket是否创建成功
printf("Client-> socket create success+++++++\n\n");
struct sockaddr_in serveaddr;
serveaddr.sin_family = AF_INET;
serveaddr.sin_port = htons(8888); //port in network byte order
serveaddr.sin_addr.s_addr = inet_addr("192.33.6.145"); //internet address
//const struct sockaddr addr = (struct sockaddr *)&serveaddr 标准化(sockaddr程序员不能直接操作)
socklen_t addrlen = sizeof(serveaddr);
int result = connect(socketFd,(struct sockaddr*)&serveaddr,addrlen);//客户端向服务端发起建立连接
if(result == 0){
printf("++++++++++++++C/S TCP/IP Connect Success+++++++++++++\n\n");
char send_buf[1024] = {0};
char recv_buf[1024] = {0};
printf("Client-> ");
while(fgets(send_buf,sizeof(send_buf),stdin)!=NULL){
printf("\n");
result = write(socketFd,send_buf,strlen(send_buf));
if(result == 0){
printf("Client-> nothing was writen to server!!\n\n");
}else if(result == -1){
printf("Client-> writen to server error!!\n\n");
}else{
//printf("Client-> writen to server success++++++++\n\n");
printf("------------Wait Serve Response------------\n\n");
result = read(socketFd,recv_buf,sizeof(recv_buf));
if(result ==0 ){
printf("Client-> Read end of file!!!\n\n");
}
else if(result == -1){
printf("Client-> Read error!!!\n\n");
}
else{
printf("Server-> ");
fputs(recv_buf,stdout);
printf("\n");
//printf("Client-> Read success+++++++++\n\n");
}
}
memset(recv_buf,0,sizeof(recv_buf));
memset(send_buf,0,sizeof(send_buf));
printf("Client-> ");
}
close(socketFd);
}
else{
printf("Client-> C/S Connect fail!!!\n\n");
}
}
else{
printf("Client-> socket create fail!!\n\n");
}
}
编译写好的client.c程序代码
gcc client.c -o client -w
网络通信
(1)先启动服务端程序,启动服务开始监听
(2)启动客户端程序,向服务端发起建立连接请求
(3)服务端接收到客户端的连接请求,建立连接
(4)客户端和服务端之间通信
客户端
服务端
Socket UDP
UDP不像TCP,无需在连接状态下交换数据,因此基于UDP的服务器端和客户端也无需经过连接过程。也就是说,不必调用 listen() 和 accept() 函数。UDP中只有创建套接字的过程和数据交换的过程。
TCP中,套接字是一对一的关系。如要向10个客户端提供服务,那么除了负责监听的套接字外,还需要创建10套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。之前解释UDP原理的时候举了邮寄包裹的例子,负责邮寄包裹的快递公司可以比喻为UDP套接字,只要有1个快递公司,就可以通过它向任意地址邮寄包裹。同样,只需1个UDP套接字就可以向任意主机传送数据。
R/W
创建好TCP套接字后,传输数据时无需再添加地址信息,因为TCP套接字将保持与对方套接字的连接。换言之,TCP套接字知道目标地址信息。但UDP套接字不会保持连接状态,每次传输数据都要添加目标地址信息,这相当于在邮寄包裹前填写收件人地址。
- 发送数据使用 sendto() 函数:
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen); //Linux
int sendto(SOCKET sock, const char *buf, int nbytes, int flags, const struct sockadr *to, int addrlen); //Windows
Linux和Windows下的 sendto() 函数类似,下面是详细参数说明:
- sock:用于传输UDP数据的套接字;
- buf:保存待传输数据的缓冲区地址;
- nbytes:带传输数据的长度(以字节计);
- flags:可选项参数,若没有可传递0;
- to:存有目标地址信息的 sockaddr 结构体变量的地址;
- addrlen:传递给参数 to 的地址值结构体变量的长度。
UDP 发送函数 sendto() 与TCP发送函数 write()/send() 的最大区别在于,sendto() 函数需要向他传递目标地址信息。
- 接收数据使用 recvfrom() 函数:
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen); //Linux
int recvfrom(SOCKET sock, char *buf, int nbytes, int flags, const struct sockaddr *from, int *addrlen); //Windows
由于UDP数据的发送端不不定,所以 recvfrom() 函数定义为可接收发送端信息的形式,具体参数如下:
- sock:用于接收UDP数据的套接字;
- buf:保存接收数据的缓冲区地址;
- nbytes:可接收的最大字节数(不能超过buf缓冲区的大小);
- flags:可选项参数,若没有可传递0;
- from:存有发送端地址信息的sockaddr结构体变量的地址;
- addrlen:保存参数 from 的结构体变量长度的变量地址值。
服务端
UDP不同于TCP,不存在请求连接和受理过程,因此在某种意义上无法明确区分服务器端和客户端,只是因为其提供服务而称为服务器端.
Serve.c
编写服务端程序代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
void main(){
int domain = AF_INET;
int type = SOCK_DGRAM;
int protocol = 0;
int socketFd = socket(domain,type,protocol); //创建socket
if(socketFd != -1){ //判断socket是否创建成功
printf("Server-> socket create success+++++++\n\n");
struct sockaddr_in serveaddr;
serveaddr.sin_family = AF_INET;
serveaddr.sin_port = htons(8888); //port in network byte order
serveaddr.sin_addr.s_addr = inet_addr("192.33.6.145"); //internet address
//const struct sockaddr addr = (struct sockaddr *)&serveaddr 标准化(sockaddr程序员不能直接操作)
socklen_t addrlen = sizeof(serveaddr);
int result = bind(socketFd,(struct sockaddr*)&serveaddr,addrlen);//socket绑定(命名)
if(result == 0){
printf("++++++++++++++C/S UDP/IP Connect Success+++++++++++++\n\n");
printf("Serve-> socket bind success++++++++++++\n\n");
//接收客户端请求
struct sockaddr_in clientaddr;
socklen_t addrlen = sizeof(clientaddr);
printf("------------Wait Client Request------------\n\n");
char recv_buf[1024] = {0};
char send_buf[1024] = {0};
while(1){
// 读取客户端发送过来的数据
result = recvfrom(socketFd, recv_buf, sizeof(recv_buf),0,(struct sockaddr *)&clientaddr, &addrlen);
if(result > 0){
printf("Client-> ");
fputs(recv_buf,stdout);
printf("\n");
printf("Serve-> "); // 向客户端回数据
fgets(send_buf,sizeof(send_buf),stdin);
printf("\n");
result = sendto(socketFd, send_buf, strlen(send_buf), 0, (struct sockaddr*)&clientaddr, addrlen);
printf("------------Wait Client Request------------\n\n");
memset(recv_buf,0,sizeof(recv_buf));
memset(send_buf,0,sizeof(send_buf));
}
}
close(socketFd);
}
else{
printf("Serve-> socket bind fail!!\n\n");
}
}else{
printf("Serve-> socket create fail!!\n\n");
}
}
编译写好的server.c程序代码
gcc serve.c -o serve -w
客户端
Client.c
编写客户端程序代码
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
void main(){
int domain = AF_INET;
int type = SOCK_DGRAM;
int protocol = 0;
int socketFd = socket(domain,type,protocol); //创建socket
if(socketFd != -1){ //判断socket是否创建成功
printf("Server-> socket create success+++++++\n\n");
struct sockaddr_in serveaddr;
serveaddr.sin_family = AF_INET;
serveaddr.sin_port = htons(8888); //port in network byte order
serveaddr.sin_addr.s_addr = inet_addr("192.33.6.145"); //internet address
//const struct sockaddr addr = (struct sockaddr *)&serveaddr 标准化(sockaddr程序员不能直接操作)
socklen_t addrlen = sizeof(serveaddr);
printf("++++++++++++++C/S UDP/IP Connect Success+++++++++++++\n\n");
char recv_buf[1024] = {0};
char send_buf[1024] = {0};
while(1){
//客户端向服务端发送数据
printf("Client-> ");
fgets(send_buf,sizeof(send_buf),stdin);
printf("\n");
int result = sendto(socketFd, send_buf, strlen(send_buf), 0, (struct sockaddr*)&serveaddr,addrlen);
if(result > 0){
printf("------------Wait Serve Response------------\n\n");
//读取服务端发送过来的数据
result = recvfrom(socketFd, recv_buf, sizeof(recv_buf),0,(struct sockaddr *)&serveaddr, &addrlen);
if(result > 0){
printf("Serve-> ");
fputs(recv_buf,stdout);
printf("\n");
}
}
memset(recv_buf,0,sizeof(recv_buf));
memset(send_buf,0,sizeof(send_buf));
}
close(socketFd);
}else{
printf("Serve-> socket create fail!!\n\n");
}
}
编译写好的client.c程序代码
gcc client.c -o client -w
网络通信
客户端和服务端之间的通信测试和TCP的一样,不测试
(1)先启动服务端程序
(2)启动客户端程序
(3)客户端和服务端之间通信
客户端
服务端
HTTP Socket编程
在Linux下使用C语言编写简单HTTP Socket客户端,实现HTTP请求访问,服务端为WWW服务器自动处理用户的Socket连接请求(TCP建立连接、数据传输)。
服务端
任意一台WWW服务器
客户端
http.c
编写客户端程序代码
#include <netinet/in.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
int sockfd;
/* [struct sockaddr_in] included by <netinet/in.h> */
struct sockaddr_in servaddr;
if (argc != 2) {
printf("usage: ./http <ip addr>\n");
exit(-1);
}
/* [socket] included by <sys/socket.h> */
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
/* [perror] included by <stdio.h> */
perror("socket error");
exit(-1);
}
/* [bzero] included by string.h */
bzero(&servaddr, sizeof(servaddr));
/* [htons] included by <arpa/inet.h> */
servaddr.sin_port = htons(80);
/* [AF_INET] included by <bits/socket.h>, but <sys/socket.h> inlude it */
servaddr.sin_family = AF_INET;
/* [inet_pton] included by <arpa/inet.h> */
if ((inet_pton(AF_INET, argv[1], &servaddr.sin_addr)) < 0) {
printf("inet_pton error\n");
exit(-1);
}
/* [connect] included by <sys/socket.h> */
if ((connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0) {
perror("connect error");
exit(-1);
}
char recvline[65536];
char buff[256] = "GET / HTTP/1.1\r\nHost: www.baidu.com\r\nConnection: close\r\n\r\n";
/* [write] and [read] included by <unistd.h> */
if ((write(sockfd, buff, strlen(buff))) < 0) {
perror("write error");
exit(-1);
}
while (read(sockfd, recvline, sizeof(recvline))) {
printf("%s", recvline);
}
printf("\n");
exit(0);
}
编译运行(运行的时候需要输入服务器IP地址,用于Socket建立连接)
Python Socket编程
Socket TCP
服务端
server.py
编写服务端服务脚本
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 15:35
@IDE : IntelliJ IDEA
"""
import socket
import threading # 多线程处理IO流
import time
import sys
def socket_service():
host = input("Input Server IP->: ")
addr = (host, 6666)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(addr)
s.listen(10) # 当前允许一个客户端链接,十个链接挂起(排队),超过10个的会返回超时信息
except socket.error as msg:
print(msg)
sys.exit(1)
print('Waiting connection...')
while 1:
conn, addr = s.accept() # 接受TCP连接,并返回新的套接字与IP地址(被动接受TCP客户端连接,(阻塞式)等待连接的到来)
t = threading.Thread(target=deal_data, args=(conn, addr))
t.start()
def deal_data(conn, addr):
print('Accept new connection from {0}'.format(addr))
# conn.settimeout(500)
print("waiting to receive client request...")
while 1:
data= conn.recv(1024)
text = data.decode('utf-8')
if text == 'exit':
break
else :
print('client {} request-> {}'.format(addr, text))
text = 'Your data was {} bytes long'.format(len(data))
data = text.encode('utf-8')
conn.send(data)
print("server response-> {}".format(data))
conn.close()
if __name__ == '__main__':
socket_service()
客户端
client.py
编写客户端通信脚本
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 15:35
@IDE : IntelliJ IDEA
"""
import socket
import sys
def socket_client():
host = input("Input Server IP->: ")
addr = (host, 6666)
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(addr)
except socket.error as msg:
print(msg)
sys.exit(1) # exit(0)无错误退出 exit(1)有错误退出
print("++++++++++++++C/S TCP/IP Connect Success+++++++++++++\n")
while 1:
data = input('client request-> ')
text = data.encode('utf-8')
s.send(text)
#输入'exit'退出
if data == 'exit':
break
else:
#getsockname返回当前套接字的信息(IP,端口号)
print("client socket info-> {}".format(s.getsockname()))
data= s.recv(1024)
text = data.decode("utf-8")
print('server {} response-> {!r}'.format(host, text))
s.close()
if __name__ == '__main__': # 如果直接运行此程序则执行if下面的语句,否则忽略这部分
socket_client()
网络通信
(1)启动服务器服务
服务器IP:192.168.68.119
(2)启动客户端
(3)客户端与服务端进行通信
客户端(客户端的请求与响应)
服务端(响应客户端的请求)
Socket UDP
服务端
server.py
编写服务端服务脚本
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 15:35
@IDE : IntelliJ IDEA
"""
import socket
byte = 1024
#两个端口要保持一致
port = 9999
host = input("Input Server IP->: ")
addr = (host, port)
#创建套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#绑定
sock.bind(addr)
print("waiting to receive client request...")
while True:
(data, addr) = sock.recvfrom(byte)
text = data.decode('utf-8')
if text == 'exit':
break
else :
print('client {} request-> {}'.format(addr, text))
text = 'Your data was {} bytes long'.format(len(data))
data = text.encode('utf-8')
sock.sendto(data, addr)
#关闭套接字
sock.close()
客户端
client.py
编写客户端通信脚本
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 15:35
@IDE : IntelliJ IDEA
"""
import socket
host = input("Input Server IP->: ")
#两个段口必须一致
port = 9999
addr = (host, port)
byte = 1024
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
data = input('Client-> ')
text = data.encode('utf-8')
sock.sendto(text, addr)
#输入'exit'退出
if data == 'exit':
break
else:
#getsockname返回当前套接字的信息(IP,端口号)
print("client socket info-> {}".format(sock.getsockname()))
data, addr = sock.recvfrom(byte)
text = data.decode("utf-8")
print('server {} replied-> {!r}'.format(addr, text))
sock.close()
网络通信
(1)启动服务器服务
服务器IP:192.168.68.119
(2)启动客户端
(3)客户端与服务端进行通信
客户端(客户端的请求与响应)
服务端(响应客户端的请求)
Python Advanced API
在这里,大家可能会发现,最原始的socket编程虽然比较简单(按照固定的流程),但是代码量是不是稍微多了点,为了提高socket编程效率,可不可以将这些使用API封装一下呢,答案是可以的(python提供有),下面以socket http请求看一下。
服务端
任意一台WWW服务器
客户端
http.py
编写客户端脚本,使用socket.create_connection()
方法直接进行socket建立连接,可以看作两步合为一步,代码量减少提高编程效率。
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 21:44
@IDE : IntelliJ IDEA
"""
import socket
r = b'''GET / HTTP/1.1
Host: www.baidu.com
'''.replace(b'\n', b'\r\n')
with socket.create_connection(("www.baidu.com", 80), timeout=5) as conn:
conn.send(r)
print(conn.recv(10240).decode())
客户端运行脚本发起http请求
从运行结果可以看到,socket正常建立连接,http请求响应正常。
https.py
上面讲的都是http 80
请求响应,那么如何实现https 443
请求响应呢,其实也很简单,HTTPS实际上就是原生socket外面套一层TLS,所以我们改改代码就可以
编写代码结合ssl.create_default_context()
在原始socket上进行连接的请求响应
# -*- coding: utf-8 -*-
"""
@Author: Qftm
@Data : 2020/3/22
@Time : 21:44
@IDE : IntelliJ IDEA
"""
import ssl
import socket
context = ssl.create_default_context()
r = b'''GET / HTTP/1.1
Host: www.baidu.com
'''.replace(b'\n', b'\r\n')
with socket.create_connection(('www.baidu.com', 443), timeout=5) as conn:
with context.wrap_socket(conn, server_hostname='www.baidu.com') as sconn:
sconn.send(r)
print(sconn.recv(10240).decode())
客户端运行脚本发起https请求
从运行结果可以看到,https 443
请求响应正常。
Linux Raw Socket编程
原始套接字
从应用开发的角度看,SOCK_STREAM、SOCK_DGRAM 这两类套接字似乎已经足够了。因为基于 TCP/IP 的应用,在传输层的确只可能建立于 TCP 或 UDP 协议之上,而这两种套接字SOCK_STREAM、SOCK_DGRAM 又分别对应于 TCP 和 UDP,所以几乎所有所有的应用都可以使用这两种套接字来实现。但是,从另外的角度,这两种套接字有一些局限:
怎样发送一个 ICMP 协议包?
怎样伪装本地的 IP 地址?
怎样实现一个新设计的协议的数据包?
这两种套接字的局限在于它们只能处理数据载荷,数据包的头部在到达用户程序的时候已经被移除了。所以,这里我们要引入另一个socket类型,原始套接字(SOCK_RAW)。原始套接字应用也很广泛,可以实现sniffer、IP 欺骗等,基于此,可以实现各种攻击。原始套接字之所以能够做到这一点,是因为它可以绕过系统内核的协议栈,使得用户可以自行构造数据包。原始套接字用于接收和发送原始数据包。 这意味着在以太网层接收的数据包将直接传递到原始套接字。 准确地说,原始套接字绕过正常的TCP/IP处理并将数据包发送到特定的用户应用程序。使用 raw套接字可以实现上至应用层的数据操作,也可以实现下至链路层的数据操作。因此也要求比应用开发更高的权限(原始套接字需要超级用户权限才能创建)。
绑定和连接操作
原始套接字一般不进行bind()操作;如果需要进行绑定,只绑定IP地址,不涉及端口。
未进行bind操作时,接收数据报时,所有数据报传递给该原始套接字;发送数据报,源IP设置为发送接口的主IP。
若绑定IP地址,接收数据报,将目的地址为本机的数据报送给该套接字;发送数据报,源IP自动设置为绑定的IP。
可以通过调用connect连接套接字。
读写原始套接字
写操作使用sendto()和sendmsg()函数完成。内核自动填充IP头部。如果通过setsockopt()函 数设置了IP_ HDRINCL选项,则必须在程序中手动填充IP头部。
如果数据报长度大于链路中最大传输单元MTU,系统自动进行分片。
读操作使用recvfrom()和recvmsg()函数完成。如果调用connect()函数连接成功后可以用recv()和read()函数接收数据报。
数据报的处理
系统接收到数据报,匹配原始套接字,将数据报copy给原始套接字。原始套接字的协议域和数据报的协议域完全匹配并且为非0值,内核将数据报传递给该原始套接字。
如果原始套接字通过bind()函数绑定到一个本地的IP地址上,则内核只会将目的IP地址和套接字绑定的IP地址匹配的数据报传递给该原始套接字。
如果原始套接字调用connect()函数和远程的IP地址连接,则内核只将源IP地址和该远程IP地址匹配的数据报传递给该原始套接字。
如果一个原始套接字在创建时protocol为0(intsocket(int domain,int type,int protocol)),并且没有调用bind()和connect()函数进行绑定和连接操作,则该原始套接字将接收所有内核传递给其他原始套接字的数据报。
下面通过编写icmp的通信过程来了解原始套接字。
Linux ICMP Socket编程
首先,确定我们要使用的socket的类型,ping是ICMP协议,目前不需要篡改IP地址,所以,我们的参数可以使用:
sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
再构造icmp数据包时需要先了解其结构
然后,查看一下ICMP的结构体(/usr/include/netinet/ip_icmp.h)
struct icmp
{
u_int8_t icmp_type; /* type of message, see below */
u_int8_t icmp_code; /* type sub code */
u_int16_t icmp_cksum; /* ones complement checksum of struct */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* gateway address */
struct ih_idseq /* echo datagram */
{
u_int16_t icd_id;
u_int16_t icd_seq;
} ih_idseq;
u_int32_t ih_void;
....
了解了ICMP的结构,下来开始编写程序实现ICMP的请求与响应
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/tcp.h>
#include <netinet/ip_icmp.h>
#include<strings.h>
#include <stdio.h>
#include <stdlib.h>
#include<string.h>
#include <arpa/inet.h>
char buff[28]={0};
int sockfd;
struct sockaddr_in target;
struct sockaddr_in source;
unsigned short in_cksum(unsigned short *addr, int len)
{
int sum=0;
unsigned short res=0;
while( len > 1) {
sum += *addr++;
len -=2;
// printf("sum is %x.\n",sum);
}
if( len == 1) {
*((unsigned char *)(&res))=*((unsigned char *)addr);
sum += res;
}
sum = (sum >>16) + (sum & 0xffff);
sum += (sum >>16) ;
res = ~sum;
return res;
}
int main(int argc, char * argv[]){
int send, recv,i;
send = 0;
recv = 0;
i = 0;
if(argc != 2){
printf("usage: %s targetip\n", argv[0]);
exit(1);
}
if(inet_aton(argv[1],&target.sin_addr)==0){
printf("bad ip address %s\n",argv[1]);
exit(1);
}
int recvfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
struct icmp * icmp = (struct icmp*)(buff);
if((sockfd=socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0)
{ perror("socket error!");exit(1); }
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_cksum = 0;
icmp->icmp_id = 2;
icmp->icmp_seq = 3;
while(send < 4)
{
send++;
icmp->icmp_seq = icmp->icmp_seq+1;
icmp->icmp_cksum = 0;
icmp->icmp_cksum = in_cksum((unsigned short *)icmp,8);
sendto(sockfd, buff, 28,0,(struct sockaddr *)&target,sizeof(target));
sleep(1);
}
struct sockaddr_in from;
int lenfrom = sizeof(from);
char recvbuff[1024];
int n;
while(recv<4){
memset(recvbuff,0,1024);
if((n = recvfrom(recvfd,recvbuff,sizeof(recvbuff),0,(struct sockaddr *)&from,&lenfrom))<0) {perror("receive error!\n");exit(1);};
struct ip *ip=(struct ip *)recvbuff;
struct icmp *icmp = (struct icmp*)(ip+1);
printf("n is %d,ip header length is %d\n ",n,ip->ip_hl);
if((n-ip->ip_hl*4)<8) {printf("Not ICMP Reply!\n");break;}
printf("ttl is %d\n",ip->ip_ttl);
printf("protocol is %d\n",ip->ip_p);
if((icmp->icmp_type==ICMP_ECHOREPLY)&&(icmp->icmp_id==2)){
printf("%d reply coming back from %s: icmp sequence=%u ttl=%d\n",recv+1,inet_ntoa(from.sin_addr),icmp->icmp_seq,ip->ip_ttl);
printf("src is %s\n",inet_ntoa(ip->ip_src));
printf("dst is %s\n",inet_ntoa(ip->ip_dst));
recv++;}
}
return 0;
}
编译运行