一、套接字的概念
Socket本身有“插座”的意思,在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么我们就可以使用文件描述符来引用套接字。
与管道类似,Linux系统将套接字封装成文件的目的是为了统一接口,使得读写套接字和读写文件的操作一致。
管道与套接字的区别是:
- 管道主要应用于本地进程间通信
- 套接字多应用于网络进程间数据的传递。
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
套接字通信原理如下图所示:
- 一个文件描述符指向一个套接字
- 一个套接字内部由内核借助两个缓冲区实现,一个写缓冲、读缓冲。
- 数据流动:发送端→sfd(文件描述符)→cfd(文件描述符)→接收端缓冲区(读缓冲)。并非图上所画直接从发送端到接收端。
在通信过程中, 套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。
TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket API。
二、套接字编程简介
网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分。
网络数据流的顺序:先发出低地址的数据,后发出高地址的数据。
TCP/IP协议规定,网络数据流应采用大端字节序(低地址高字节)。如果本地主机是小端字节序的,则需要考虑网络字节序和主机字节序的转换问题。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
uint32_t htonl(uint32_t hostlong); // 本地字节序转网络字节序(IP)
uint16_t htons(uint16_t hostshort); // 本地字节序转网络字节序(Port)
uint32_t ntohl(uint32_t netlong); // 网络字节序转本地字节序(IP)
uint16_t ntohs(uint16_t netshort); // 网络字节序转本地字节序(Port)
- h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
- IP地址是32位,端口号是16位
- 因为IP地址是string类型,而字节序转换函数接收参数类型是无符号32位长整数,所以在调用字节序转换函数前,还需要调用atoi函数对IP地址进行类型转换。(了解即可,后面有封装的IP 地址转换函数可直接完成IP地址的类型转换和字节序转换)
IP地址转换函数
// 本地字节序(string IP) ---> 网络字节序
int inet_pton(int af, const char *src, void *dst);
- 函数名为inet_pton,意思是ip to network,将**String类型的IP地址由本地字节序转换无符号32位长整型的网络字节序**
- af:IP协议类型,
- 可选值为AF_INET、AF_INET6
- 分别代表IPv4和IPv6两种协议。
- AF_INET、AF_INET6是宏定义。
- src:传入参数,要转换的String类型的IP地址(点分十进制)
- dst:传出参数,转换后的网络字节序的IP地址
- 函数返回值是int类型
- 转换成功返回1
- 异常返回0,说明src指向的不是一个有效的ip地址。
- 失败返回-1
如果函数接口有指针参数,既可以把指针所指向的数据传给函数使用(称为传入参数),也可以由函数填充指针所指的内存空间,传回给调用者使用(称为传出参数)
因为void *可以指向任何类型的数据,所以void *指针一般被称为通用指针或者泛指针,或者叫做万能指针。void指针可以赋值给其他任意类型的指针,其他任意类型的指针也可以赋值给void指针,它们之间赋值不需要强制类型转换。
// 网络字节序 ---> 本地字节序(string IP)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 函数名为inet_ntop,意思是network to ip,将IP地址由无符号32位长整型的网络字节序转换为String类型的本地字节序
- af:AF_INET、AF_INET6
- src:传入参数,要转换的无符号32位长整型的IP地址的网络字节序
- dst:传出参数,转换后的String类型的IP地址的本地字节序
- size:dst的大小,也就是转换后的String类型的IP地址的本地字节序的长度
- 返回值:
- 成功:dst
- 失败:NULL
三、套接字地址结构
- 因为网络编程函数的诞生早于IPv4协议,那时候使用的是sockaddr结构体,后来对该结构体改进产生了后面三个结构体类型
- sockaddr_in中的in代表internet
- sockaddr_in是IPv4使用的,sockaddr_in6是IPv6使用的
- sockaddr_un是本地进程间通信套接字(了解即可)
- 由于网络编程函数的传入参数仍是旧的sockaddr结构体类型,所以在使用网络编程函数时需要使sockaddr_in结构体类型通过强制类型转换为sockaddr结构体类型
- c语言中强制类型转换:按照变量的原数据类型从内存取出数据,然后按照另一数据类型的来解释数据
- sockaddr_in类型的结构体将sockaddr类型的结构体中最后14字节大小的地址数据进行了进一步的细分,将端口号和IP地址分别存在了两个变量中
- 16位的地址类型(2字节)
- 16位的端口号(2字节)
- 32位的IP地址(4字节)
- 8字节填充
struct sockaddr
{
unsigned short sa_family; // 2字节,地址族,AF_xxx
char sa_data[14]; // 14字节,包含套接字中的目标地址和端口信息
};
struct sockaddr_in {
sa_family_t sin_family; // address family:AF_INET
in_port_t sin_port; // port in network byte order
struct in_addr sin_addr; // internet address
char sin_zero[8] // unused
};
// internet address
struct in_addr {
uint32_t s_addr;
};
【例】以bind函数为例演示套接字地址结构的使用
// addr是sockaddr_in类型的变量
struct sockaddr_in addr;
// 对addr变量赋值
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(9527);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 将addr强制类型转换后传入用sockaddr做参数的bind函数
bind(fd, (struct sockaddr *)&addr, size);
INADDR_ANY:
是一个宏,自动取系统中有效的任意IP地址,
让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址。
二进制类型,也就是长整型,所以不需要类型转换。
总结
- sockaddr和sockaddr_in二者长度一样,都是16个字节,即占用的内存大小是一致的,前2个字节都表示地址类型,不同的是:
内部结构
- sockaddr用最后的14个字节来表示sa_data,
- sockaddr_in把最后14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、IP地址、填充字节
- sin_zero填充字节使sockaddr_in和sockaddr保持一样大小都是16字节
用途
- sockaddr是给操作系统使用的,因为早期的系统调用函数指定了参数类型必须是sockaddr
- sockaddr_in是给程序员使用的,因为sockaddr_in区分了IP地址和端口,使用更方便
- 程序员把类型、IP地址、端口填充给sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数
四、基本TCP套接字编程
网络通信流程图
- 上图表示:借助TCP实现一个CS模型所需要的网络套接字函数
- 一个网络通信的建立需要三个套接字:一对用来通信,一个用来监听
- socket()创建一个socket
- bind()为socket绑定ip+port
- listen()设置监听上限,即同时跟服务器建立socket连接的数量
- accept():阻塞监听客户端连接,创建一个新的socket用来与客户端通信
- connect():客户端使用现有的socket与服务器建立连接,如果不使用bind绑定客户端地址结构,采用“隐式绑定”,系统自动分配ip+port
五、TCP协议通讯流程
- 连接的建立:三次握手的过程
- 服务器调用socket()、bind()、listen()完成初始化后,调用**accept()阻塞等待**,处于监听端口的状态
- 客户端调用socket()初始化后,调用**connect()发出SYN段(SYN状态标志位有效)并阻塞等待**服务器应答
- 服务器应答一个SYN-ACK段
- 客户端收到后从**connect()返回**,同时应答一个ACK段,服务器收到ACK段后从**accept()返回**
- 数据传输的过程
- 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。
- 服务器从accept()返回后立刻调用read(),读socket的缓冲区就像读管道一样,如果没有数据到达**read()就阻塞等待**
- 客户端调用write()发送请求给服务器,然后,调用**read()阻塞**等待服务器的应答,
- 服务器收到客户端的请求进行读取后**read()返回**,处理请求后,调用write()将处理结果发回给客户端,再次调用**read()阻塞**等待下一条请求
- 客户端收到后从**read()返回**,发送下一条请求,如此循环下去
- 连接的释放:
- 如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。
- 注意:
- 半关闭状态:通信双方中,只有一端关闭通信
- 调用close()后,调用方关闭通信,调用方的读和写缓冲区都关闭
- 调用shutdown()后,调用方关闭通信,但可指定函数参数来只关闭写缓冲或读缓冲
六、TCP状态转换
- 实线表示主动发起连接一端的状态
- 虚线表示被动接收连接一端的状态
主动发起连接请求端
- CLOSED:表示初始状态
- SYN_SENT:主动打开状态,表示客户端已发送SYN报文。这个状态与SYN_RCVD遥相呼应,当客户端发送SYN报文(第一次握手)后,随即进入到SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。
- ESTABLISHED:数据传输状态,表示连接已经建立。当客户端收到服务器应答的SYN-ACK报文(第二次握手),并回复ACK报文(第三次握手)后,随即进入到ESTABLISHED状态
主动关闭连接请求端
- FIN_WAIT_1:主动关闭连接的一方,发出FIN报文后进入该状态
- FIN_WAIT_2:半关闭状态,该状态下的socket只能接收数据,不能发送数据。当主动关闭连接的一方,发出FIN收到ACK以后进入该状态
- TIME_WAIT: 当收到了对方的FIN报文,并发送出了ACK报文后,进入到TIME_WAIT状态,
- CLOSE:TIME_WAIT状态等2MSL后即可回到CLOSED初始状态
- 如果FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态
- 只有主动关闭连接方会经历该状态
- 2MSL时长:一定出现在【主动关闭连接请求端】。对应TIME_WAIT状态,保证最后一个ACK能成功被对端接收。(等待2MSL时长期间,对端没收到我发的ACK,对端会再次发送FIN请求)
被动接收连接
- CLOSED:表示初始状态
- LISTEN:监听状态,表示服务器端可以接受连接请求
- SYN_RCVD: 当接收到SYN报文(第一次握手),并回复SYN-ACK报文(第二次握手)后,进入SYN_RCVD状态
- ESTABLISHED:数据传输状态,表示连接已经建立。当服务器接收到客户端应答的ACK报文(第三次握手)后,随即进入到ESTABLISHED状态
被动关闭连接
- CLOSE_WAIT:当接收到主动关闭方发送的FIN并应答一个ACK报文后,进入到CLOSE_WAIT状态。接下来查看是否还有数据发送给对方,如果没有,则发送FIN报文给对方
- 说明对端(主动关闭连接端)处于半关闭状态
- LAST_ACK: 当被动关闭一方在发送FIN报文后,进入LAST_ACK状态
- CLOSE:当被动关闭一方在LAST_ACK状态接收到对方发送的ACK后进入CLOSE初始状态,即关闭连接
socket函数
// 创建一个套接字
int socket(int domain, int type, int protocol);
- domain:IP协议,AF_INET、AF_INET6、AF_UNIX/AF_LOCAL
- type:数据传输协议,流式协议SOCK_STREAM,报式协议SOCK_DGRAM
- protocol:数据传输协议中的代表协议。
- 默认0,根据type选择指定协议。
- 流式协议SOCK_STREAM代表协议TCP
- 报式协议SOCK_DGRAM代表协议UDP
- 返回值
- 成功:新套接字对应的文件描述符
- 失败:-1 errno,
【例】用socket()函数创建一个套接字,使用IPv4协议、流式协议,返回该套接字的文件描述符
fd = socket(AF_INET, SOCK_STREAM, 0)
bind函数
// 给socket绑定一个地址结构(ip+port)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:套接字对应的文件描述符
- addr:(struct sockaddr)&addr
- addr要先初始化
- addrlen:sizeof(addr),地址结构的大小
- 返回值
- 成功:0
- 失败:-1 errno
【例】用bind函数绑定socket地址结构
// addr是sockaddr_in类型的变量
struct sockaddr_in addr;
// 对addr变量赋值
addr.sin_family = AF_INET/AF_INET6;
addr.sin_port = htons(9527);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 将addr强制类型转换后传入用sockaddr做参数的bind函数
bind(fd, (struct sockaddr *)&addr, sizeof(addr));
listen函数
// 设置同时与服务器建立连接的上限数(同时进行三次握手的客户端)
int listen(int sockfd, int backlog);
- sockfd:套接字对应的文件描述符
- backlog:上限数值,最大值128
- 返回值
- 成功:0
- 失败:-1 errno
accept函数
// 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:套接字对应的文件描述符
- addr:传出参数。成功与服务器建立连接的客户端的地址结构(ip+port)
- addrlen:传入传出参数。&client_addr_len
- 传入的是调用者提供的addr缓冲区的长度以避免缓冲区溢出问题
- 使用前需要先定义addr缓冲区的长度
- 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址
- 返回值
- 成功:能与服务器进行数据通信的客户端socket对应的文件描述符
- 失败:-1 errno
connect函数
// 客户端使用现有的socket与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:客户端套接字对应的文件描述符
- addr:传入参数。服务器的地址结构
- addrlen:服务器的地址结构的大小
- 返回值
- 0:成功
- -1 errno:失败
- 如果不使用bind绑定客户端地址结构,采用“隐式绑定”,系统自动分配ip+port
【例】用connect函数请求连接服务器
struct sockaddr_in srv_addr;
// 配置服务器的地址结构
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = 9527;
inet_pton(AF_INET, {String类型IP地址的本地字节序}, &srv_addr.sin_addr.s_addr);
connect(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr))
setsockopt函数
int setsockopt(int sockfd, int level, int optname, const void *optval, ,socklen_t optlen);
- 函数功能:用来设置参数sockfd所指定的socket的状态
- level:代表欲设置的网络层,
- SOL_SOCKET:socket层
- optname:代表欲设置的选项参数
- SO_REUSEADDR:地址复用
- optval:代表欲设置的值
- 1:启用
- 0:不启用
- optlen:为optval的长度
七、多进程并发服务器
使用多进程并发服务器时要注意:
- 防止僵尸进程
- 捕捉SIGCHLD信号
// 多进程并发服务器
Socket();
Bind();
Listen();
while(1){
connectfd = Accept(); // 接收客户端连接请求
pid = fork(); // 创建子进程
if ( pid == 0 ){ // 子进程
Close(listenfd); // 关闭用于监听的套接字
read();
// 将读到的字节进行操作
write();
} else if (pid > 0) {
Close(connectfd); // 关闭用于与客户端通信的套接字
// 用信号捕捉函数回收子进程的相关代码
continue;
}
}
- Socket():创建监听套接字listenfd
listenfd是用来建立连接的套接字
当客户端有连接请求时,服务器会借助listenfd创建一个connectfd用来与客户端进行通信数据传输。listenfd被解放出来,用于监听其他客户端的请求
在多进程并发服务器中,父进程用于监听客户端请求,子进程用于与客户端通信
- Bind():绑定地址结构 struct socketaddr_in servaddr;
- Listen():设置同时与服务器建立连接的上限数(同时进行三次握手的客户端)
- Accept():阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的connectfd
- 在多进程并发服务器中,每个connectfd都有一个子进程与之对应;
- 在多进程并发服务器中,由于支持多个客户端与服务器建立连接,所以Accept应该循环执行,一直监听客户端的连接请求
- fork():产生子进程
- Close():fork产生子进程后,子进程和父进程一样,都有listenfd和connectfd两个套接字。子进程不需要监听套接字,只需要与客户端通信的套接字,所以关闭listenfd;父进程不需要与客户端通信的套接字,只需要监听套接字用来监听客户端请求,所以关闭connectfd
- read():读缓冲
- 将读到的字节进行操作
- write():写缓冲
- signal():信号捕捉,如果捕捉到SIGCHLD信号,说明子进程结束,就调用处理函数,完成子进程回收
- waitpid:因为waitpid()可以设置不阻塞,所以使用waitpid()。
- 由于设置waitpid()为非阻塞,所以当子进程没结束时,返回0,需要再次执行去回收,防止所有子进程都变为僵尸进程,所以需要设置为循环执行waitpid(),子进程一旦结束,一定要立刻回收。但循环执行的话,父进程可能会阻塞于waitpid(),此时如果有新的客户端连接请求,父进程无法响应。
- 结局办法:将上述操作放入信号处理函数中
- continue:结束本次循环,父进程监听下一个客户端的连接请求
八、多路I/O转接服务器
原理:借助内核,select或poll来监听客户端连接、数据通信事件
select函数
int select (int maxfd + 1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout);
- 函数功能:用于监视文件描述符的变化情况——读/写/异常
- nfds:监听的所有文件描述符中最大的一个+1,此参数会告诉内核监听多少个文件描述符
- readset:传入传出参数,读文件描述符的监听集合,只监听该集合中文件描述符的读事件
- 传入传出参数:传入的是想要监听的文件描述符,函数执行完,传出的是满足对应事件的文件描述符
- writeset:传入传出参数,写文件描述符的监听集合,只监听该集合中文件描述符的写事件
- NULL:不监听文件描述符的写事件
- exceptset:传入传出参数,异常文件描述符的监听集合,只监听该集合中文件描述符的异常事件
- NULL:不监听文件描述符的异常事件
- timeout:阻塞监听时间
- NULL:阻塞,一直等
- timeval:timeval是一个结构体,有两个成员变量:tv_sec和tv_usec,分别是秒和微秒
- >0:设置成员变量为固定时间
- 0:不阻塞,检查描述字后立即返回,轮询
- 返回值:
- >0:所有的文件描述符监听集合中满足对应事件的总个数
- 0:没有满足对应事件的文件描述符
- -1:errno
优点
跨平台:win、linux、macOS、Unix、类Unix、mips
缺点
监听上限受文件描述符限制,最大 1024
无法直接定位满足监听事件的文件描述符,只能用循环轮询或自己构建数组保存各个connfd
// 清空文件描述符集合中(所有位置零)
void FD_ZERO (fd_set *fdset);
// 将一个文件描述符添加到文件描述符的监听集合中
void FD_SET (int fd,fd_set *fdset);
// 将一个文件描述符从文件描述符的监听集合中移除
void FD_CLR (int fd,fd_set *fdset);
// 判断一个文件描述符是否在文件描述符的监听集合中,在的话返回1;不在返回0
int FD_ISSET(int fd,fd_set *fdset);
思路分析:
int maxfd = 0;
lfd = socket(); // 创建套接字
maxfd = lfd;
bind(); // 绑定地址结构
listen(); // 设置监听上限
fd_set rset,allset; // rset 读事件文件描述符集合 allset 用来暂存(b)
FD_ZERO(&allset); // 将allset监听集合清空
FD_SET(lfd, &allset); // 将 lfd 添加至allset监听集合中。
while(1) {
rset = allset; // rset作为传入传出参数
ret = select(lfd+1, &rset, NULL, NULL, NULL); // 监听文件描述符集合对应事件
if(ret > 0) { // 有监听的描述符满足对应事件
if (FD_ISSET(lfd, &rset)) { // rset作为传出参数;1在,0不在
cfd = accept(); // 建立连接,返回用于通信的文件描述符
maxfd = cfd;
FD_SET(cfd, &allset); // 添加cfd到监听集合中
}
for(i = lfd+1; i <= 最大文件描述符; i++) { // 遍历
if (FD_ISSET(i, &rset)){ // 有read、write事件发生
read()
操作字节;
write();
}
}
}
}
poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct polled {
int fd;
short events;
short revents
}
函数功能:用于监视文件描述符的变化情况——读/写/异常
fds:监听的文件描述符的结构体【数组】,此处的*就是首地址
fd:监听的文件描述符
events:监听事件,用宏定义
POLLIN:有数据可读
POLLOUT:写事件
POLLERR:异常事件
POLLRDNORM:有普通数据可读
revents:初始化为0,如果满足对应事件则返回非0,即POLLIN、POLLOUT、POLLERR
nfds:监听数组中的实际监听个数
timeout:阻塞监听时间,单位:毫秒
-1:阻塞,一直等
0:不阻塞,检查描述字后立即返回,
>0:等待指定毫秒数
返回值:
>0:所有的文件描述符监听集合中满足对应事件的总个数
0:没有满足对应事件的文件描述符
-1:errno
优点
自带数组结构,分离了传入传出参数
可以拓展监听上限,超出 1024限制
缺点
不能跨平台,只能UNIX/LINUX,不能WINDOWS
无法直接定位满足监听事件的文件描述符,只能用循环轮询或自己构建数组保存各个connfd
九、基本UDP套接字编程
TCP和UDP对比
TCP:传输控制协议
面向连接的,可靠数据包传输。
对于不稳定的网络层,采取完全弥补的通信方式——丢包重传
优点:
数据流量稳定:在建立TCP连接后,数据传输流量是稳定的
传输速度稳定:在建立TCP连接后,数据传输速度是稳定的
传输顺序一致:在网络环境不发生变化的情况下,各个数据包经过的路由节点一致,数据报到达顺序与发送时一致
缺点:
传输速度慢
传输效率低
资源开销大
使用场景
数据的完整型要求较高,不追求效率
大数据传输、文件传输。
UDP:用户数据报协议
无连接的,不可靠的数据报传递
对于不稳定的网络层,采取完全不弥补的通信方式——默认还原网络状况
优点:
传输速度快
传输速率高
资源开销小
缺点:
数据流量不稳定
传输速度不稳定
传输顺序不一致:各个数据报经过的路由节点可能不一致,数据报到达顺序可能与发送时不一致
使用场景
对时效性要求较高场合,稳定性其次
游戏、视频会议、视频电话
可以通过采用应用层数据校验协议,弥补udp的不足,保证数据包有效传递
UDP通信流程
//server:
lfd = socket(AF_INET, SOCK_DGRAM, 0);
bind();
while(1){
recvfrom();
// 操作字节数据
sendto();
}
close();
//client:
connfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto('服务器的地址结构','地址结构大小');
recvfrom();
// 操作字节数据
close();
- 因为不用三次握手,所以服务器不需要accept(),客户端不需要connect()
- 因为listen()是用来设置服务器同时与客户端进行三次握手的上限,所以listen()可有可无
- 由于listenfd不负责通信,所以read()被替换为recvfrom(),write()被替换为sendto()
- 客户端直接使用sendto()向服务器发起连接
recvfrom函数
int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr,socklen_t *addrlen);
- 函数功能:接收socket传来的数据, 并把数据存到buf指向的内存空间
- sockfd:对服务器来说就是listenfd
- buf:接收数据的缓冲区
- len:缓冲区大小
- flags:调用操作方式
- 0:常规操作,与read()相同
- src_addr:传出参数,传出对端地址结构
- addrlen:传入传出参数,地址结构长度
- 返回值:
- >0:成功接收到的字节数
- 0:对端关闭连接
- -1:errno
sendto函数
int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
- 函数功能:将数据由socket传给对方主机
- sockfd:套接字
- buf:存储数据的缓冲区
- len:数据长度
- flags:调用操作方式
- 0:常规操作,与writ()相同
- src_addr:传入参数,传入目标地址结构
- addrlen:传入传出参数,地址结构长度
- 返回值:
- >0:成功写出的字节数
- 0:对端关闭连接
- -1:errno