套接字
每一个 Socket 都用一个半相关描述:
{协议,本地地址,本地端口}
一个完整的 Socket 则用一个相关描述
{协议,本地地址,本地端口,远程地址,远程端口}
每一个 Socket 有一个本地的唯一 Socket 号,由操作系统分配。
套接字有三种类型:
流式套接字( SOCK_STREAM), 数据报套接字( SOCK_DGRAM)及原始套接字。
流式套接字( SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流 。如果你通过流式套接字发送了顺序的数据:“ 1”、“2 ”。那么数据到达远程时候的顺序也是“1 ”、“2 ”。
它**使用了 TCP ( TheTransmission Control Protocol)协议 ,TCP 保证了你的数据传输是正确的,并且是顺序的。 **
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1PLkECNE-1686293772759)(C:/Users/20220715013/AppData/Roaming/Typora/typora-user-images/image-20220919190344389.png)]
数据报套接字( SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。原始套接字允许对低层协议如 IP 或 ICMP 直接访问,主要用于新的网络协议实现的测试等。
- 如果你发送了一个数据报,它可能不会到达。
- 它可能会以不同的顺序到达。
- 如果它到达了,它包含的数据中可能存在错误。
**数据报套接字也使用 IP,但是它不使用 TCP,它使用使用者数据报协议 UDP。 **
( UDP)不像流式套接字那样维护一个打开的连接,你只需要把数据打成一个包,把远程的 IP 贴上去,然后把这个包发送出去。这个过程是不需要建立连接的。
每个使用 UDP的程序都要有自己的对数据进行确认的协议,以此来保证程序可以正常运行。
比如, TFTP 协议定义了对于每一个发送出
去的数据包,远程在接受到之后都要回送一个数据包告诉本地程序:“我已经拿到了!”(一
个 “ ACK” 包)。如果数据包发的送者在 5 秒内没有的得到回应,它就会重新发送这个
数据包直到数据包接受者回送了 “ ACK” 信号。
无连接服务器一般都是面向事务处理的,一个请求一个应答就完成了客户程序与服务程序之间的相互作用。
原始套接字
原始套接字主要用于一些协议的开发,可以进行比较底层的操作。它功能强大,但是没有上面介绍的两种套接字使用方便,一般的程序也涉及不到原始套接字。
模型
客户/服务器模型(c/s)模型
服务器:提供计算服务的设备
- 永久提供服务
- 永久性访问地址/域名
客户机:请求计算服务的主机
- 与服务器通信,使用服务器提供的服务
- 间歇性接入网络
- 可能使用动态IP地址
- 不与其他客户机直接通信
应用:web,文件传输FTP,远程登录,电子邮件
P2P模型
- 不存在永远在线的服务器
- 每个主机既可以提供服务也可以请求服务
- 任意端系统/节点之间可以直接通信
- 节点间歇性接入网络
- 节点可能改变IP地址
- 可扩展性好(不用担心资源不够和带宽受限的问题)
- 网络健壮性强(网络不容易瘫痪)
TCP/IP协议
一个协议定义了两个应用程序或是计算机之间能够进行互相通讯,对于
其中的每一个(应用程序或计算机)都保证使用同样的标准。
应用层
对应用程序的通信提供服务
应用层重要协议:FTP\SMTP\POP3\HTTP\DNS
功能:
- 文件传输、访问和管理
- 电子邮件
- 虚拟终端
- 查询服务和远程作业登录
TCP/IP 协议可以根据提供的不同的服务分为几组:
控制数据的协议
TCP( 传输控制协议 Transmission Control Protocol)以连接为基础,也就是说两台电脑必须先建立一个连接,然后才能传输数据。
UDP(使用者数据报协议 User Datagram Protocol)它是一个无连接服务,数据可以直接发送而不必在两台电脑之间建立一个网络连接。
它和有连接的 TCP 相比,占用带宽少,但是你不知道你的数据是否真正到达了你的客户端,而客户端收到的数据也不知道是否还是原来的发送顺序 。
数据路由协议
路由协议分析数据包的地址并且决定传输数据到目的电脑最佳路线。他们也可以把大的数据分成几部分,并且在目的地再把他们组合起来。
IP(因特网协议 Internet Protocol) 处理实际上传输数据。
ICMP(因特网控制信息协议 Internet Control Message Protocol) 处理 IP 的状态信息,比如能影响路由决策的数据错误或改变。
RIP(路由信息协议 Routing Information Protocol) 它是几个决定信息传输的最佳路由路线协议中的一个。
OSPF( Open Shortest Path First) 一个用来决定路由的协议。网络地址协议决定了命名电脑地址的方法:使用一个唯一的数字和一个字母名字。
ARP(地址决定协议 Address Resolution Protocol) 确定网络上一台电脑的数字地址。
DNS(域名系统 Domain Name System) 从机器的名字确定一个机器的数字地址。
RARP(反向地址决定协议 Reverse Address Resolution Protocol) 确定网络上一台计算机的地址,和 ARP(地址决定协议 Address Resolution Protocol)正好相反。
用户服务
BOOTP(启动协议 Boot Protocol) 由网络服务器上取得启动信息,然后将本地的网络计算机启动。
FTP(文件传输协议 File Transfer Protocol) 通过国际互连网从一台计算机上传输一个或多个文件到另外一台计算机。
TELNET(远程登陆) 允许一个远程登陆,使用者可以从网络上的一台机器通过 TELNET连线到另一台机器,就像使用者直接在本地操作一样。
EGP(外部网关协议 Exterior Gateway Protocol) 为外部网络传输路由信息。
GGP(网关到网关协议 Gateway-to-Gateway Protocol) 在网关和网关之间传输路由协议。
IGP(内部网关协议 Interior Gateway Protocol) 在内部网络传输路由信息。
其他协议(也为网络提供了重要的服务)
NFS(网络文件系统 Network File System) 允许将一台机器的目录被另一台机器上的用户 安装( Mount)到自己的机器上,就像是对本地文件系统进行操作一样进行各式各样的操作。
NIS(网络信息服务 Network Information Service) 对整个网络用户的用户名、密码进行统一管理,简化在 NIS 服务下整个网络登陆的用户名/密码检查。
RPC(远程过程调用 Remote Procedure Call) 通过它可以允许远程的应用程序通过简单的、有效的手段联系本地的应用程序,反之也是。
SMTP(简单邮件传输协议 Simple Mail Transfer Protocol) 一个专门为电子邮件在多台机器中传输的协议,平时发邮件的 SMTP 服务器提供的必然服务。
SNMP(简单网络管理协议 Simple Network Management Protocol) 这是一项为超级用户准备的服务,超级用户可以通过它来进行简单的网络管理。
socket
一个套接字可以这样来解释:它是通过标准的 UNIX 文件描述符和其他的程序通讯的一个方法。
一个文件描述符只是一个简单的整形数值,代表一个被打开的文件(这里的文件是广义的文件,并不只代表不同的磁盘文件,它可以代表一个网络上的连接,一个先进先出队列,一个终端显示屏幕,以及其他的一切)。在 UNIX 系统中任何东西都是一个文件 。
套接字描述符就是一个文件描述符 。
write() 和 read() 是可以对套接字描述符进行操作的,但是,通过使用 send() 和 recv() 函数,可以对网络数据的传输进行更好的控制!
基本结构
struct sockaddr
struct sockaddr {
unsigned short sa_family; /* address族, AF_xxx */
char sa_data[14]; /* 14 bytes的协议地址 */
};
sa_family 一般来说,都是 “ AFINET”。
sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一起的。
另外一个相似的结构 struct sockaddr_in:
struct sockaddr_in {
short int sin_family; /* Internet地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* Internet地址 */
unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小) */
};
**sin_zero[8] 是为了是两个结构在内存中具有相同的尺寸,使用 sockaddr_in 的时候要把 sin_zero 全部设成零值(使用 bzero()或 memset()函数)。 **
一个指向 struct sockaddr_in 的指针可以声明指向一个 sturct sockaddr 的结构。所以虽然socket() 函数需要一个 structaddr * ,也可以给他一个 sockaddr_in * 。
struct in_addr
/* 因特网地址 (a structure for historical reasons) */
struct in_addr {
unsigned long s_addr;
};
如 果 已经 声 明 了 一 个 “ ina ” 作 为 一 个 struct sockaddr_in 的 结 构 , 那 么“ ina.sin_addr.s_addr”就是 4 个字节的 IP 地址(按网络字节顺序排放)。
基本转换函数
- htons()—— “Host to Network Short”主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4 bytes)
- htonl()—— “Host to Network Long” 主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8 bytes)
- ntohs()—— “Network to Host Short “ 网络字节顺序转换为主机字节顺序(对无符号短型进行操作 4 bytes)
- ntohl()—— “Network to Host Long “ 网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8 bytes)
IP地址转换
假设有一个 struct sockaddr_in ina,并且你的 IP 是 166.111.69.52 ,需要
把 IP 存储到 ina 中。可以使用的函数:inet_addr() ,它能够把一个用数字和点表示 IP 地址的字符串转换成一个无符号长整型。
ina.sin_addr.s_addr = inet_addr( “166.111.69.52”) ;
inet_addr() 返回的地址已经是网络字节顺序了,不用再去调用 htonl() 函数 。
如果有一个 struct in_addr 并且你想把它代表的 IP 地址打印出来(按照 数字.数字.数字.数字的格式)……这里,你可以使用函数 inet_ntoa()(“ ntoa”代表“ Network to ASCII”):
printf( “%s”, inet_ntoa(ina.sin_addr)) ;
inet_ntoa() 使用 struct in_addr 作为一个参数,不是一个长整型值。
l inet_ntoa() 返回一个字符指针,它指向一个定义在函数 inet_ntoa() 中的 static 类型字符串。所以每次你调用 inet_ntoa(),都会改变最后一次调用inet_ntoa() 函数时得到的结果。
基本套接字调用
socket() 函数
取得套接字描述符
socket 函数的定义是下面这样子的:
#include <sys/types.h>
#include <sys/socket.h>c
int socket( int domain , int type , int protocol) ;
domain 需要被设置为 “ AF_INET”
type 设置为“ SOCK_STREAM”或是“ SOCK_DGRAM”
protocol 设置为 0
注意: domain 参数可以取除了“ AF_INET ”外的很多值, types 参数也可以取除了“ SOCK_STREAM”或“ SOCK_DGRAM”的另外类型。
socket()函数只是简单的返回一个以后可以使用的套接字描述符。如果发生错误,socket()函数返回 –1 。全局变量 errno 将被设置为错误代码。
socket_select()
使用IO多路转接函数委托内核检测服务器端所有文件描述符,检测过程会导致进程/线程阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出。
根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理:
- 监听的文件描述符:和客户端建立连接
- 此时调用accept()不会导致程序阻塞,因为监听的文件描述符已就绪
- 通信的文件描述符:调用通信函数和已建立连接的客户端通信
- 对这些文件描述符进行下一轮的检测
socket_select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
struct timeval *timeout)
一个文件描述符对应两块内存,一块内存是读缓冲区,一块内存是写缓冲区。
setsockopt()
int setsockopt(
SOCKET s,
int level,
int optname,
const char* optval,
int optlen
);
s(套接字): 指向一个打开的套接口描写叙述字
level:(级别): 指定选项代码的类型。
SOL_SOCKET: 基本套接口
IPPROTO_IP: IPv4套接口
IPPROTO_IPV6: IPv6套接口
IPPROTO_TCP: TCP套接口
optname(选项名): 选项名称
optval(选项值): 是一个指向变量的指针 类型:整形,套接口结构, 其它结构类型:linger{}, timeval{ }
optlen(选项长度) :optval 的大小
bind() 函数
bind()函数可以指定一个套接字使用的端口
当使用socket()函数得到一个套接字描述符,需要为socket绑定一个端口
- 需要进行端口监听 listen()操作,等待接受一个连入请求的时候,一般都需要经过这一步。
- 如果只需要连接一台服务器,进行connect()操作可以不绑定端口。
bind()的系统调用声明如下:
#include <sys/types.h>
#include <sys/socket.h>
int bind (int sockfd , struct sockaddr *my_addr , int addrlen) ;
sockfd 是由 socket()函数返回的套接字描述符。
my_addr 是一个指向 struct sockaddr 的指针,包含有关你的地址的信息:名称、端口和 IP 地址。
addrlen 可以设置为 sizeof(struct sockaddr)。
当 bind()函数调用错误的时候,它也是返回–1 作为错误发生的标志。 errn 的值为错误代码。
没有使用bind()函数的时候,Linux 内核自动为我们选择了一个没有被使用的本地端口。
connect()函数
connect() 函数的定义是这样的:
#include <sys/types.h>
#include <sys/socket.h>
int connect (int sockfd, struct sockaddr *serv_addr, int addrlen);
connect()的三个参数意义如下:
sockfd 套接字文件描述符,由 socket()函数返回的。
serv_addr 是一个存储远程计算机的 IP 地址和端口信息的结构。
addrlen 应该是 sizeof(struct sockaddr)。
一定要检测 connect()的返回值:如果发生了错误(比如无法连接到远程主
机,或是远程主机的指定端口无法进行连接等)它将会返回错误值 –1 。 全局变量 errno将会存储错误代码。
在面向连接的协议的程序中,服务器执行以下函数:
- 调用 socket()函数创建一个套接字。
- 调用 bind()函数把自己绑定在一个地址上。
- 调用 listen()函数侦听连接。
- 调用 accept()函数接受所有引入的请求。
- 调用 recv()函数获取引入的信息然后调用 send()回答。
listen() 函数
listen()函数是等待别人连接,进行系统侦听请求的函数。当有人连接你的时候,你有两步需要做:
- 通过 listen()函数等待连接请求;
- 使用 accept()函数来处理。
listen()函数调用是非常简单的。函数声明如下:
#include <sys/socket.h>
int listen(int sockfd, int backlog);
listen()函数的参数意义如下:
sockfd 是一个套接字描述符,由 socket()系统调用获得。
backlog 是未经过处理的连接请求队列可以容纳的最大数目。
backlog 的意思是:每一个连入请求都要进入一个连入请求队列,等待listen 的程序调用 accept()函数来接受这个连接。当系统还没有调用 accept()函数的时候,如果有很多连接,那么本地能够等待的最大数目就是 backlog 的数值。可以将其设成 5 到 10 之间的数值(推荐)。
listen()如果返回 –1 ,那么说明在 listen()的执行过程中发生了错误。全局变量 errno 中存储了错误代码。
用了listen()说明是在等待别人连接,需要用bind()绑定端口。
accept()函数
当有人尝试用connect()连接你的端口时,他的连接被listen()加入等待队列等待accept(),然后你调用accept()函数告诉他准备连接,accept()函数将返回一个新的套接字描述符,这个套接字描述符代表你们两给之间的连接,第一个套接字描述符仍然在原来的端口上listen();
得到的那个新的套接字描述符就可以用来进行 send()操作和 recv()操作
下面是 accept()函数的声明:
#include <sys/socket.h>
int accept(int sockfd, void *addr, int *addrlen);
accept()函数的参数意义如下:
sockfd 是正在 listen() 的一个套接字描述符。
addr 一般是一个指向 struct sockaddr_in 结构的指针;里面存储着远程连接过来的计算机的信息(比如远程计算机的 IP 地址和端口)。
addrlen 是一个本地的整型数值,在它的地址传给 accept() 前 它 的 值 应 该 是sizeof(struct sockaddr_in); accept()不会在 addr 中存储多余 addrlen bytes 大小的数据。如果accept()函数在 addr 中存储的数据量不足 addrlen,则 accept()函数会改变 addrlen 的值来反应这个情况。
如果调用 accept()失败的话, accept()函数会返回 –1 来表明调用失败,同时全局变量 errno 将会存储错误代码。
send()、 recv()函数
send()
这两个函数是最基本的,通过连接的套接字流进行通讯的函数。
send() 函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int send(int sockfd, const void *msg, int len, int flags);
send 的参数含义如下:
sockfd 是代表你与远程程序连接的套接字描述符。
msg 是一个指针,指向你想发送的信息的地址。
len 是你想发送信息的长度。
flags 发送标记。一般都设为 0。
send()函数在调用后会返回它真正发送数据的长度。
send() 所发送的数据可能少于你给它的参数所指定的长度!
因为如果你给 send()的参数中包含的数据的长度远远大于 send()所能一次发送的数据,则 send()函数只发送它所能发送的最大数据长度。
如果 send()函数的返回值小于 len 的话,则你需要再次发送剩下的数据。如果包足够小(小于 1K),那么 send()一般都会一次发送完的。
send()函数如果发生错误,则返回 –1 ,错误代码存储在全局变
量 errno 中。
recv()
下面是 recv()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int recv(int sockfd, void *buf, int len, unsigned int flags) ;
recv()的参数含义如下:
sockfd 是你要读取数据的套接字描述符。
buf 是一个指针,指向你能存储数据的内存缓存区域。
len 是缓存区的最大尺寸。
flags 是 recv() 函数的一个标志,一般都为 0。
recv() 返回它所真正收到的数据的长度。(也就是存到 buf 中数据的长度)。
如果返回–1 则代表发生了错误(比如网络以外中断、对方关闭了套接字连接等),全局变量 errno 里面存储了错误代码。
sendto() 和 recvfrom() 函数
sendto()
这两个函数是进行无连接的 UDP 通讯时使用的。使用这两个函数,则数据会在没有建立过任何连接的网络上传输。因为数据报套接字无法对远程主机进行连接。
sendto()函数的声明:
#include <sys/types.h>
#include <sys/socket.h>
int sendto( int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen) ;
sockfd 是代表你与远程程序连接的套接字描述符。
msg 是一个指针,指向你想发送的信息的地址。
len 是你想发送信息的长度。
flags 发送标记。一般都设为 0。
to 是一个指向 struct sockaddr 结构的指针,里面包含了远程主机的 IP 地址和端口数据。
tolen 只是指出了 struct sockaddr 在内存中的大小 sizeof(struct sockaddr)。
sendto()返回它所真正发送的字节数(当然也和 send()一样,它所真正发送的字节数可能小于你所给它的数据的字节数)。 发生错误的时候,返回 –1 ,同时全局变量 errno 存储了错误代码。
recvfrom()
recvfrom()的声明为:
#include <sys/types.h>
#include <sys/socket.h>
int recvfrom(int sockfd, void *buf, int len, unsigned int flags
struct sockaddr *from, int *fromlen);
sockfd 是你要读取数据的套接字描述符。
buf 是一个指针,指向你能存储数据的内存缓存区域。
len 是缓存区的最大尺寸。
flags 是 recv() 函数的一个标志,一般都为 0
from 是一个本地指针,指向一个 struct sockaddr 的结构(里面存有源 IP 地址和端口数).
fromlen 是一个指向一个 int 型数据的指针,它的大小应该是 sizeof ( struct sockaddr).当函数返回的时候, formlen 指向的数据是 form 指向的 struct sockaddr 的实际大小.
recvfrom() 返回它接收到的字节数,如果发生了错误,它就返回–1 ,全局变量 errno存储了错误代码.
close()和 shutdown()函数
close()
程序进行网络传输完毕后,需要关闭这个套接字描述符所表示的连接。
只需要使用标准的关闭文件的函数: close()。
使用方法:
close(sockfd);
执行 close()之后,套接字将不会在允许进行读操作和写操作。任何有关对套接字描述符进行读和写的操作都会接收到一个错误。
shutdown()
如果想对网络套接字的关闭进行进一步的操作的话,可以使用函数 shutdown()。 它允许你进行单向的关闭操作,或是全部禁止掉。
shutdown()的声明为:
#include <sys/socket.h>
int shutdown( int sockfd, int how) ;
它的参数含义如下:
sockfd 是一个你所想关闭的套接字描述符
how 可以取下面的值。 0 表示不允许以后数据的接收操; 1 表示不允许以后数据的发送操作; 2 表示和 close()一样,不允许以后的任何操作(包括接收,发送数据)
shutdown() 如果执行成功将返回 0,如果在调用过程中发生了错误,它将返回–1,全局变量 errno 中存储了错误代码。
setsockopt() 和 getsockopt() 函数
Linux 所提供的 socket 库含有一个错误( bug)。此错误表现为你不能为一个套接字重新启用同一个端口号(也就是在程序重新启动后,就不能把套接字绑定在原来的端口上了),即使在正常关闭该套接字以后。
问题就是 Linux 内核在一个绑定套接字的进程结束后从不把端口标记为未用。在大多数 Linux/UNIX 系统中,端口可以被一个进程重复使用,甚至可以被其它进程使用。
在 Linux 中绕开这个问题的办法是,当套接字已经打开但尚未有连接的时候用setsockopt()系统调用在其上设定选项( options)。 setsockopt() 调用设置选项而 getsockopt()从给定的套接字取得选项。
#include<sys/types.h>
#include<sys/socket.h>
int getsockopt(int sockfd, int level, int name, char *value, int *optlen);
int setsockopt(int sockfd, int level, int name, char *value, int *optlen);
下面是两个调用的参数说明:
sockfd 必须是一个已打开的套接字。
level 是函数所使用的协议标准( protocol level)( TCP/IP 协议使用 IPPROTO_TCP,套接字标准的选项实用 SOL_SOCKET)。
name 选项在套接字说明书中( man page) 有详细说明。
value 指向为 getsockopt()函数所获取的值, setsockopt()函数所设置的值的地址。
optlen 指针指向一个整数,该整数包含参数以字节计算的长度。
getpeername()函数
这个函数可以取得一个已经连接上的套接字的远程信息(比如 IP 地址和端口),告诉你在远程和你连接的究竟是谁。
它的声明为:
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);
下面是参数说明:
sockfd 是你想取得远程信息的那个套接字描述符。
addr 是一个指向 struct sockaddr (或是 struct sockaddr_in) 的指针。
addrlen 是一个指向 int 的指针,应该赋于 sizeof(struct sockaddr)的大小。
如果在函数执行过程中出现了错误,函数将返回 –1 ,并且错误代码储存在全局变量errno 中。
当拥有了远程连接用户的 IP 地址,就可以使用 inet_ntoa() 或gethostbyaddr()来输出信息或是做进一步的处理。
gethostname()函数
gethostname()函数可以取得本地主机的信息。它比 getpeername()要容易使用一些。
它返回正在执行它的计算机的名字。返回的这个名字可以被 gethostbyname()函数使用,由此可以得到本地主机的 IP 地址。
下面是它的声明:
#include <unistd.h>
int gethostname(char *hostname, size_t size);
参数说明如下:
hostname 是一个指向字符数组的指针,当函数返回的时候,它里面的数据就是本地的主机的名字.
size 是 hostname 指向的数组的长度.
函数如果成功执行,它返回 0,如果出现错误,则返回–1,全局变量 errno 中存储着错误代码。