再次用到socket编程,将socket相关的知识点做了简单整理,根据网络上大家的整理,又做了一些调整和汇总。
sokect常见的API大致有列表里面这么多,不同平台的实现可能有些微的差别,下面对常用API的参数和用法做了逐个整理。
socket(int, int, int)
socketpair(int, int, int, int [2])
bind(int, const sockaddr *, socklen_t)
connect(int, const sockaddr *, socklen_t)
listen(int, int)
accept(int, sockaddr *, socklen_t *)
accept4(int, sockaddr *, socklen_t *, int)
send(int, const void *, size_t, int)
sendto(int, const void *, size_t, int, const sockaddr *, socklen_t)
recv(int, void *, size_t, int)
recvfrom(int, void *, size_t, int, sockaddr *, socklen_t *)
shutdown(int, int)
setsockopt(int, int, int, const void *, socklen_t)
getsockopt(int, int, int, void *, socklen_t *)
getsockname(int, sockaddr *, socklen_t *)
getpeername(int, sockaddr *, socklen_t *)
recvmsg(int, msghdr *, int)
sendmsg(int, msghdr *, int)
int socket(int domain, int type, int protocol);
socket
函数对应于普通文件的打开操作。
普通文件的打开操作返回一个文件描述符,而**socket()**用于创建一个socket
描述符(socket descriptor),它唯一标识一个socket
。
socket
描述符跟文件描述符一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
参数:
domain
: 即协议域,又称为协议族(family),常用的协议组有:
type
: 指定socket类型,常用的socket类型有:
protocol
:指定协议,常用的协议有:
注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。
许多时候内核会我们自动绑定一个地址,调用connect()、listen()时系统会自动随机分配一个端口。
然而有时用户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由bind()
的函数完成。
参数:
sockfd
: 即socket
描述符,它是通过socket()
函数创建了,唯一标识一个socket
。bind()
函数就是将给这个描述符绑定一个名字。addr
: 一个const struct sockaddr *
指针,指向要绑定给sockfd
的协议地址。addrlen
:地址的长度。比如alsa-lib/aserver/aserver.c中make_inet_socket函数的代码:
struct sockaddr_in addr;
int sock;
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock < 0) {
int result = -errno;
SYSERROR("socket failed");
return result;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
return -errno;
}
addr这个地址结构是根据地址创建socket时的地址协议族的不同而不同,这里是sockaddr_in
,将addr初始化后转化为struct sockaddr *
类型作为参数传入。
struct sockaddr_in {
short int sin_family; /* 通信类型 2bytes */
unsigned short int sin_port; /* 端口 2bytes */
struct in_addr sin_addr; /* Internet 地址 4bytes */
unsigned char sin_zero[8];
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
原来的sockaddr
的格式是(16个字节):
struct sockaddr {
unsigned short sa_family; /* 地址家族, AF_xxx */
char sa_data[14]; /* 14字节协议地址 */
};
可以看出sockaddr_in
和sockaddr
二者长度一样,都是16个字节,即占用的内存大小是一致的,因此可以互相转化。
sockaddr和sockaddr_in对比
sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
sockaddr_in是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in
结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数。
sockaddr_in用于socket定义和赋值。
sockaddr用于函数参数。
Unix域对应的是sockaddr_un
,总共120个字节:
#define UNIX_PATH_MAX 108
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* pathname */
};
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器。
而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调 用,而是在connect()时由系统随机生成一个。
int listen(int sockfd, int backlog);
listen函数在一般在调用bind之后,调用accept之前调用。
作为一个服务器,在调用socket(),bind()之后就会调用listen()
来监听这个socket。
listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。
当调用listen之后,服务器进程就可以调用accept来接受一个外来的请求。
参数:
sockfd
::socket描述符,唯一的id。backlog
:相应socket可以排队的最大连接个数。int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
客户端通过调用connect函数来建立与TCP服务器的连接。
客户端调用connect()发出连接请求,服务器端就会接收到这个请求。
connect()函数返回后并不进行数据交换。而是要等服务器端 accept 之后才能进行数据交换。
成功返回0,失败返回-1。当客户端调用 connect()函数之后,发生以下情况之一才会返回:
服务器端接收连接请求
发生断网的异常情况而终端连接请求
参数:
sockfd
:客户端建立socket函数的返回值。addr
:指定所要连接的服务器的地址,服务器的socket地址(要和服务端的实际IP地址以及绑定的端口一致才可以)。addrlen
:socket协议地址的长度,可由sizeof()计算得出。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8aODEq6-1676817320838)(.images/1659149924677533.png)]
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。
TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。
然后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
参数:
sockfd
:服务器的socket描述符。addr
:指向struct sockaddr *
的指针,用于返回给客户端的协议地址。addrlen
:socket协议地址的长度。int send(int sockfd, const void *buf, int len, int flags);
参数:
sockfd
:想要发送数据的套接字描述符。buf
:指向发送数据缓冲区的指针。flags
:一般设置为0.send()返回实际发送的数据的字节数,可能小于你要求发送的长度,在错误的时候返回SOCKET_ERROR(-1)
,并设置errno。
send()只是发送它可能发送的数据,如果send()返回的数据和len
不匹配,就需要将剩下的数据发送完。
send()先比较待发送数据的长度len和套接字sockfd的发送缓冲的长度, 如果len大于sockfd的发送缓冲区的长度,该函数返回SOCKET_ERROR。
如果len小于或者等于sockfd的发送缓冲区的长度,那么send()先检查协议是否正在发送s的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送sockfd的发送缓冲中的数据或者sockfd的发送缓冲中没有数据,那么send就比较sockfd的发送缓冲区的剩余空间和len:
如果len大于剩余空间大小,send()就一直等待协议把sockfd的发送缓冲中的数据发送完。
如果len小于剩余空间大小,send()就仅仅把buf中的数据copy到剩余空间里(注意并不是send()把sockfd的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。
如果协议在后续的传送过程中出现网络错误的话,那么下一个Socket函数就会返回SOCKET_ERROR。
每一个除send外的Socket函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR。
在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
异步socket的send函数在网络刚刚断开时还能发送返回相应的字节数,同时使用select检测也是可写的,但是过几秒钟之后,再send就会出错了,返回-1。select也不能检测出可写了。
int recv(int sockfd, FAR void *buf, size_t len, int flags)
参数:
sockfd
:要读的套接字描述符。buf
:读取数据的缓冲区。len
:缓冲区的最大长度。flags
:一般设置为0.recv()返回实际读入缓冲的数据的字节数。或者在错误的时候返回SOCKET_ERROR(-1)
,同时设置errno。
当应用程序调用recv函数时,recv先等待sockfd的发送缓冲中的数据被协议传送完毕。
如果sockfd的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字sockfd的接收缓冲区,如果sockfd接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,只到协议把数据接收完毕。
当协议把数据接收完毕,recv函数就把sockfd的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以在这种情况下要调用几次recv()函数才能把sockfd的接收缓冲中的数据copy完。
recv()函数仅仅是copy数据,真正的接收数据是协议来完成的。
如果recv函数在等待协议接收数据时网络中断了,那么它返回0。在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。
int shutdown(int sockfd,int howto);
参数:
sockfd
:socket文件描述符。howto
:如何控制断连过程。
终止网络连接的通用方法是调用close函数,但使用shutdown能更好的控制断连过程。
int sendto(
int sockfd,
const void *buf,
int len,
unsigned int flags,
const struct sockaddr *dest_addr,
socklen_t addrlen
);
参数:
sockfd
:socket文件描述符。buf
:发送的数据缓冲区。len
:发送的数据长度。flags
:该参数一般为0。dest_addr
:(可选)指明数据发送的目标地址。addrlen
:(可选)目标地址长度,一般为:sizeof(struct sockaddr_in)
。对于sendto()函数,成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno中。
一般情况下,send()、recv()在TCP协议下使用,sendto()、recvfrom()在UDP协议下使用,也可以在TCP协议下使用,不过用的很少。
在无连接的socket方式下,由于本地socket并没有与远端机器建立连接,所以在发送数据时应指明目的地址,所以该函数比send()函数多了两个参数dest_addr和addrlen。
int recvfrom(
int sockfd,
void *buf,
int len,
unsigned int lags,
struct sockaddr *src_addr,
socklen_t *addrlen
);
参数:
sockfd
:socket文件描述符。buf
:接收数据缓冲区。len
:接收缓冲区长度。flags
:调用操作方式。src_addr
:(可选)如果src_addr不为NULL,并且底层协议提供了源地址,则会填充此源地址。当src_addr为NULL时,不填写任何内容。addrlen
:(可选)指向from缓冲区长度值。对于recvfrom()
函数,成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中。
recvfrom()
用于从套接字接收消息,并且可用于接收套接字上的数据,无论它是否面向连接。
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
参数:
fdarray
:一个结构体,用来保存各个描述符的相关状态。
nfds
:fdarray数组的大小,即里面包含有效成员的数量。
timeout
:设定的超时时间(以毫秒为单位)。
select()函数和poll()函数均是主要用来处理多路I/O复用的情况。比如一个服务器既想等待输入终端到来,又想等待若干个套接字有客户请求到达,这时候就需要借助select或者poll函数了。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds
:本参数忽略,仅起到兼容作用。readfds
:指针,指向一组等待可读性检查的套接口。writefds
:指针,指向一组等待可写性检查的套接口。exceptfds
:指针,指向一组等待错误检查的套接口。timeout
:select()
最多等待时间,对阻塞操作则为NULL。Select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
使用select就可以完成非阻塞方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况。
int fcntl(int sockfd, int cmd, ... /* arg */);
参数:
sockfd
:套接字描述符。cmd
:操作命令。arg
:与cmd关联的数据。返回值:
如果成功,返回的值将取决于指定的 cmd。 如果不成功,fcntl() 返回-1并将errno设置为以下之一:
fcntl()
实际上是一个系统调用,不是socket的API,用于对打开的文件描述符执行操作,比如获取或设置文件描述符标志(更改O_APPEND或O_NONBLOCK状态标志)。
有以下操作命令可供使用:
close-on-exec
标志,如果FD_CLOEXEC
位是 0,执行exec
相关函数后文件句柄保持打开,反之则关闭。如将sockfd设置为非阻塞方式:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
将sockfd设置为非阻塞方式:
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);
int close(int sockfd);
close()
一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用。
“TCP是一种流模式的协议,UDP是一种数据报模式的协议”
为什么这样说,这是基于TCP和UDP的工作方式定义的。
TCP的定义
TCP的工作方式可以这样理解:
大概就像输油管道一样,需要建立连接,数据大小不固定:
- 发送方和接收方建立连接(三次握手)
- 发送方和接收方通过TCP连接交换字节流。例如发送方药发送80字节的数据,如果发送端先传10字节,又传20字节,再传50字节,连接的另一方将无法了解发方每次发送了多少字节。接收方只要自己的接收缓存没有塞满,TCP接收方将有多少就收多少。发送方将字节流放到TCP连接上,接收方就接受多少信息。
- 发送方发送完消息会通知接收方关闭连接(四次挥手)
UDP的定义
UDP的工作方式可以这样理解:
大概就像发短信一样,不需要建立连接,数据大小固定:
发送方将要发送的数据封装到数据包中,发送给接收方,接收方会接收这个数据包。
手撕Linux Socket——Socket原理与实践分析
秒懂流模式和数据报模式