一、Socket是什么
socket中文翻译“套接字”,提供了一种标准化的方法,使不同的计算机之间可以建立连接并在连接上进行数据传输。它可以在不同的网络层次上操作,如传输层(例如TCP和UDP)或网络层(例如IP)。
套接字提供了一组函数(通常是系统调用),这些函数可以用于创建、绑定、连接、监听和发送/接收数据等操作。
socket是全双工的,这就表明通信的双方建立socket连接后,是可以同时进行读写操作的,因为socket的读缓冲区和写缓冲区是独立的两个通道,互不影响。
二、常用函数
1.socket()函数
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
函数描述:创建一个socket描述符,唯一标志一个socket,用于后续的读写操作
- 参数解释:
- domain:协议域(协议簇),决定了socket的地址类型,常用的协议有AF_INET(IPV4互联网协议簇)、PF_INET6/AF_INET6(IPV6互联网协议簇)、AF_UNIX/AF_UNIX(要用一个绝对路径名作为地址)。
- type:指socket类型,有面向连接的套接字(SOCK_STREAM)和面向消息的套接字(SOCK_DGRAM),其中面向连接的套接字可以理解成TCP协议,数据稳定、按序传输,不存在数据边界,且收发数据在套接字内部有缓冲,所以服务器和客户端进行I/O操作时并不会马上调用,可能分多次调用;面向消息的套接字可以看做UDP,特点:快速传输、有数据边界、数据可能丢失、传输数据大小受限。
- protocol:指计算机间通信中使用的协议信息。一般都可以为0(当protocol为0时,会自动选择type类型对应的默认协议。),如果同一协议簇中存在多个数据传输方式相同的协议,则才用第三个参数。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等。type和protocol并不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。
- 返回值:
- 成功,返回新建的文件描述符。
- 失败, 返回-1, 并设置环境变量errno。
被socket函数返回的套接字,它是一个主动连接的套接字,也就是此时系统假设用户会对这个套接字调用connect函数,期待它主动与其它进程连接。
2.bind()函数
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数描述:用于服务端将通信的地址和端口绑定到 socket上。只有这样,流经该 ip地址和端口的数据才能交给该socket处理。把一个地址族中的特定地址赋给socket(如果一个TCP客户或者服务器不用bind绑定一个端口,则调用connect或listen时,内核就要为相应的套接字选择一个临时端口(一般TCP客户端可这样做,TCP服务器不可,因为服务器的端口是大家熟知的))
- 参数解释
- sockfd:指的是通过socket()创建的描述字,唯一标识一个socket。
- addr:一个指针,指向要绑定的协议地址。
- addrlen:对应的是地址的长度。
- 返回值:
- 成功,返回0。
- 失败,返回-1,并设置环境变量errno。
注意:
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
sockaddr
sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号。
struct sockaddr{
sa_family_t sin_family; //地址族(Address Family),也就是地址类型
char sa_data[14]; //IP地址和端口号
};
typedef unsigned short int sa_family_t;
sockaddr 结构体共16个字节,是socket编程中标准的地址结构体。几乎所有的socket API均使用sockaddr作为其地址信息存储结构。但由于定义它的时候还处于IPv4的年代,并没有预料到会有IPv6的诞生,sockaddr大小只有16字节,是无法存储IPv6 128位的IP地址的,因此在socket扩展中加入了sockaddr_storage通用地址结构。
在实际应用中,bind()第二个参数sockaddr都是由下面的某种结构体转换指针而来。
sockaddr_storage
14 字节的sa_data 根本无法容纳多数协议族的地址值。
因此,Linux定义了下面这个新的通用的 socket 地址结构体,这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的,该结构体128字节。
#include <bits/sockaddr.h>
typedef unsigned short sa_family_t;
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t ss_family;
char __ss_padding[_SS_PADSIZE]; //(128 - (sizeof (unsigned short int)) - sizeof (unsigned long int))
unsigned long int __ss_align;
};
该结构体具有以下特点:
- ss_family 字段表示地址族,即协议簇(如 AF_INET、AF_INET6 等)。
- _SS_PADSIZE 字段是内部填充字段,保证 sockaddr_storage 结构体总共为 128 字节。
由于其内部填充字段,使得 sockaddr_storage 能够容纳任意类型的套接字地址,因此可以在网络编程中灵活使用。
sockaddr_in
该结构体用于IPv4, 共16字节。
struct sockaddr_in {
sa_family_t sin_family; //地址簇,取值AF_INET(IPV4),AF_INET6(IPV6)
unit16_t sin_port; //16位TCP/UDP端口号,以网络字节序保存
struct in_addr sin_addr; //32位IP地址,以网络字节序保存
char sin_zero[8]; //不使用,但一般初始化为0
}
typedef uint32_t in_addr_t;
struct in_addr {
In_addr_t s_addr; //32位IP地址
}
sockaddr_in6
该结构体用于IPv6, 共28字节。
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
uint16_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
};
sockaddr_un
UNIX 本地域协议族使用如下专用的 socket 地址结构体
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* pathname */
};
3.listen()函数
#include <sys/socket.h>
int listen(int sockfd, int backlog);
函数描述:listen将socket设置为被动监听类型,等待客户的连接请求。把一个未连接的套接字转换成被动套接字,使其可以接受来自其他主动套接字的连接请求,并限制Server程序调用accept函数之前的最大连接数。在服务器编程中,用户希望这个套接字可以接受外来的连接请求,也就是被动等待用户来连接。由于系统默认时认为一个套接字是主动连接的,所以需要通过某种方式来告诉系统,用户进程通过系统调用listen来完成这件事。
- 参数解释:
- sockfd:要监听的socket描述字。
- backlog:可以排队的最大连接个数。内核进程在自己的空间里维护的一个跟踪已完成连接但服务器进程还没有接手处理或正在进行的连接的队列的大小。backlog告诉内核使用这个数值作为队列大小的上限。
- 返回值:
- 成功,返回0。
- 失败,返回-1,并设置环境变量errno。
4.accept()函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函数描述:TCP服务器在依次调用socket()、bind()、listen()后,开始监听指定socket地址,接着调用accept()获取请求,建立连接;TCP客户端依次调用socket()、connect()就可以发送连接请求。
- 参数解释:
- sockfd:服务器的socket描述字。
- addr:指向 struct sockaddr 的指针,用于返回客户端的协议地址。
- addrlen:协议地址的长度。
- 返回值:
- 成功,返回由内核自动生成的一个全新的描述字,且已经与客户建立好TCP连接。
- 失败,返回-1,并设置环境变量errno。
注意:
内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字。与此客户端进行通信只能用此函数返回的描述符。
5.connect()函数
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函数描述:客户端调用connect()来连接服务器,进行信息传输。
-参数解释:
- sockfd:客户端的socket描述字。
- addr:服务器的socket地址,这个和bind中的地址使用方法一致。
- addrlen:socket地址的长度。
- 返回值:
- 成功,返回0。
- 失败,返回-1,并设置环境变量errno。
6.read()、write()函数
这里列出好几组在socket上进行数据读取的函数。
这里有好几组函数用于在socket上读取和写入数据。
https://blog.csdn.net/weixin_45525272/article/details/107732407
6.1 write()/read()
read()和write()函数并不是专门为socket编程设计的,socket编程读写操作推荐用send()/recv()函数。而read()和write()适用于所有的文件读写,因为Linux下一切皆文件,所以每个套接字都可以看作一个文件,也可以使用read()和write()函数进行读写操作。
send()/write()主要用在TCP。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
write()
用途描述:期望将用户空间的buf内nbytes字节的数据拷贝到文件描述符fd指定的内核缓冲区中。
注:并不是发送到对端socket,这是内核通过网络干的事情,write写入内核缓冲区,内核通过网络在某个时间把其中的数据发送到对端sockert文件描述符指定的对端内核缓冲区中,send也一样,不是发送。
返回值:
- -1:出错并设置errno。
- =0:连接关闭。
- >0:写入数据大小。
缓冲区足够写入全部数据时,都是全部写入并返回大小。
缓冲区不够全部写入时,先写入部分,阻塞write会阻塞直到全部写入,非阻塞会返回写入的数据大小。非阻塞下返回-1并且errno == EINTR(信号中断) || errno == EWOULDBLOCK(将要阻塞) || errno == EAGAIN(无空间可写)时,认为连接是正常的。
这个函数多用于TCP socket。 可用于UDP oscket,但是不推荐,因为不带目的地址,必须先使用connect绑定目标地址等限制条件。
read()
用途描述:期望将内核中缓冲区的nbytes字节的数据拷贝到用户进程空间的buf中。一般用于TCP socket。
注:并不是从对端socket接收数据,这是内核通过网络干的事情,内核在某个时间接收到了对端socket发送过来的数据,read从内核缓冲区中读取这些数据。recv也一样,不是接收。
- 返回值:
- -1,出错并设置errno。
- =0,连接关闭。
- >0,读到的数据大小。
如果缓冲区有数据,都是读走并返回大小。
如果缓冲区没有数据,阻塞模式下read会阻塞等待数据,非阻塞下,返回-1,当errno == EINTR(信号中断) || errno == EWOULDBLOCK(将要阻塞) || errno == EAGAIN(无数据可读)时,认为连接是正常的。
6.2 recv()/send() TCP专用
一般情况下,send()、recv()在TCP协议下使用。写入数据到发送缓冲区;从接收缓冲区读取数据。
#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);
6.3 sendto()/recvfrom() 多用于UDP
sendto()、recvfrom()在UDP协议下使用,因为UDP通信没有连接概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指内容,addrlen则指定地址长度。
recvfrom、sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要吧最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我么已经和对方建立了连接,所以已经知道其socket地址),所以也可以在TCP协议下使用,不过用的很少,显得复杂。
sendto()
sendto() 函数是一个系统调用,用于发送数据到一个指定的地址。它经常与无连接的数据报协议,如UDP,一起使用。不像 send() 函数只能发送数据到一个预先建立连接的远端,sendto() 允许在每次发送操作时指定目的地址。
// 发送数据函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- 参数:
- sockfd: 基于 udp 的通信的文件描述符
- buf: 这个指针指向的内存中存储了要发送的数据
- len: 要发送的数据的实际长度
- flags: 设置套接字属性,一般使用默认属性,指定为 0 即可
- dest_addr: 接收数据的一端对应的地址信息,大端的 IP 和端口
- addrlen: 参数 dest_addr 指向的内存大小
- 返回值:
- 成功。返回实际发送的字节数
- 失败。返回 -1
recvfrom()
recvfrom() 函数是一个系统调用,用于从套接字接收数据。该函数通常与无连接的数据报服务(如 UDP)一起使用,但也可以与其他类型的套接字使用。与简单的 recv() 函数不同,recvfrom() 可以返回数据来源的地址信息。
// 接收数据, 如果没有数据,该函数阻塞
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- 参数
- sockfd: 基于 udp的通信的文件描述符
- buf: 指针指向的地址用来存储接收的数据
- len:buf指针指向的内存的容量,最多能存储多少字节
- flags: 设置套接字属性,一般使用默认属性,指定为 0 即可
- src_addr: 发送数据的一端的地址信息,IP 和端口都存储在这里边,是大端存储的
如果这个参数中的信息对当前业务处理没有用处,可以指定为 NULL, 不保存这些信息 - addrlen: 类似于 accept () 函数的最后一个参数,是一个传入传出参数
传入的是 src_addr 参数指向的内存的大小,传出的也是这块内存的大小
如果 src_addr 参数指定为 NULL, 这个参数也指定为 NULL 即可
- 返回值:
- 成功,返回接收的字节数
- 若失败,返回 - 1,设置errorno
- 若返回0,表示对端关闭
6.4 readv()/writev() TCP专用,分散数据
readv函数将数据从文件描述符读到分散的内存块中,即分散读;
writev函数将多块分散的内存数据一并写人文件描述符中,即集中写。
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
- 参数:readv和writev的第一个参数fd是个文件描述符,第二个参数是指向iovec数据结构的一个指针,其中iov_base为缓冲区首地址,iov_len为缓冲区长度,参数iovcnt指定了iovec的个数。
- 返回值:
- 成功,返回读、写的总字节数
- 失败,时返回-1并设置相应的errno。
示例:
#include <sys/uio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
char *str0 = "hello ";
char *str1 = "world\n";
struct iovec iov[2];
ssize_t nwritten;
iov[0].iov_base = str0;
iov[0].iov_len = strlen(str0);
iov[1].iov_base = str1;
iov[1].iov_len = strlen(str1);
nwritten = writev(STDOUT_FILENO, iov, 2);
return 0;
}
6.5 recvmsg()/sendmsg()
这是最复杂最强大的两个函数,之前的几组函数都可以切换成这个函数来调用,这里就不细讲这两个函数了。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
struct msghdr {
void *msg_name; /* protocol address */
socklen_t msg_namelen; /* size of protocol address */
struct iovec *msg_iov; /* scatter/gatter array */
int msg_iovlen /* elements in msg_iov array */
void *msg_control; /* ancillary data (cmsghdr struct) */
socklen_t msg_controllen; /* length of ancillary data */
int msg_flags; /* flags returned by recvmsg */
};
7.close()函数
#include <unistd.h>
int close(int fd);
函数描述:数据传输结束时调用close()函数。关闭socket的读和写通道。
三、其他函数
setsockopt 函数
添加socket额外的选项
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
支持的选项如下:
#define SO_DEBUG 1 -- 打开或关闭调试信息
#define SO_REUSEADDR 2 -- 打开或关闭地址复用功能
#define SO_TYPE 3 --
#define SO_ERROR 4
#define SO_DONTROUTE 5
#define SO_BROADCAST 6
#define SO_SNDBUF 7 -- 设置发送缓冲区的大小
#define SO_RCVBUF 8 -- 设置接收缓冲区的大小
#define SO_KEEPALIVE 9 -- 套接字保活
#define SO_OOBINLINE 10
#define SO_NO_CHECK 11
#define SO_PRIORITY 12 -- 设置在套接字发送的所有包的协议定义优先权
#define SO_LINGER 13
#define SO_BSDCOMPAT 14
#define SO_REUSEPORT 15
#define SO_PASSCRED 16
#define SO_PEERCRED 17
#define SO_RCVLOWAT 18
#define SO_SNDLOWAT 19
#define SO_RCVTIMEO 20 -- 设置接收超时时间
#define SO_SNDTIMEO 21 -- 设置发送超时时间
#define SO_ACCEPTCONN 30
#define SO_SNDBUFFORCE 32
#define SO_RCVBUFFORCE 33
#define SO_PROTOCOL 38
我们常用的开启地址复用,设置缓冲区大小。
shutdown()
/* Shut down all or part of the connection open on socket FD.
HOW determines what to shut down:
SHUT_RD = No more receptions;
SHUT_WR = No more transmissions;
SHUT_RDWR = No more receptions or transmissions.
Returns 0 on success, -1 for errors. */
extern int shutdown (int __fd, int __how) __THROW;
SHUT_RD:断开输入流。套接字无法接收数据(即使缓冲区收到数据也会被清除),无法调用输入相关函数。
SHUT_WD:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
SHUT_RDWR:同时断开I/O流。相当于分两次调用shutdown(),其中一次以SHUT_RD为参数,另一次以SHUT_WR为参数。
字节序转换
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);