一篇20000个字精通Linux套接字编程

一、套接字的概念

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协议类型,
  1. 可选值为AF_INET、AF_INET6
  2. 分别代表IPv4和IPv6两种协议。
  3. AF_INET、AF_INET6是宏定义。
  • src:传入参数,要转换的String类型的IP地址(点分十进制)
  • dst:传出参数,转换后的网络字节序的IP地址
  • 函数返回值是int类型
  1. 转换成功返回1
  2. 异常返回0,说明src指向的不是一个有效的ip地址。
  3. 失败返回-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地址的本地字节序的长度
  • 返回值:
  1. 成功:dst
  2. 失败:NULL


三、套接字地址结构

  • 因为网络编程函数的诞生早于IPv4协议,那时候使用的是sockaddr结构体,后来对该结构体改进产生了后面三个结构体类型
  1. sockaddr_in中的in代表internet
  2. sockaddr_in是IPv4使用的,sockaddr_in6是IPv6使用的
  3. sockaddr_un是本地进程间通信套接字(了解即可)
  • 由于网络编程函数的传入参数仍是旧的sockaddr结构体类型,所以在使用网络编程函数时需要使sockaddr_in结构体类型通过强制类型转换为sockaddr结构体类型
  1. c语言中强制类型转换:按照变量的原数据类型从内存取出数据,然后按照另一数据类型的来解释数据
  • sockaddr_in类型的结构体将sockaddr类型的结构体中最后14字节大小的地址数据进行了进一步的细分,将端口号和IP地址分别存在了两个变量中
  1. 16位的地址类型(2字节)
  2. 16位的端口号(2字节)
  3. 32位的IP地址(4字节)
  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个字节都表示地址类型,不同的是:

内部结构

  1. sockaddr用最后的14个字节来表示sa_data,
  2. sockaddr_in把最后14个字节拆分成sin_port, sin_addr和sin_zero分别表示端口、IP地址、填充字节
  3. sin_zero填充字节使sockaddr_in和sockaddr保持一样大小都是16字节

用途

  1. sockaddr是给操作系统使用的,因为早期的系统调用函数指定了参数类型必须是sockaddr
  2. 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协议通讯流程

  • 连接的建立:三次握手的过程
  1. 服务器调用socket()、bind()、listen()完成初始化后,调用**accept()阻塞等待**,处于监听端口的状态
  2. 客户端调用socket()初始化后,调用**connect()发出SYN段(SYN状态标志位有效)并阻塞等待**服务器应答
  3. 服务器应答一个SYN-ACK段
  4. 客户端收到后从**connect()返回**,同时应答一个ACK段,服务器收到ACK段后从**accept()返回**
  • 数据传输的过程
  1. 建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。
  2. 服务器从accept()返回后立刻调用read(),读socket的缓冲区就像读管道一样,如果没有数据到达**read()就阻塞等待**
  3. 客户端调用write()发送请求给服务器,然后,调用**read()阻塞**等待服务器的应答,
  4. 服务器收到客户端的请求进行读取后**read()返回**,处理请求后,调用write()将处理结果发回给客户端,再次调用**read()阻塞**等待下一条请求
  5. 客户端收到后从**read()返回**,发送下一条请求,如此循环下去
  • 连接的释放:
  1. 如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。
  2. 注意:
  3. 半关闭状态:通信双方中,只有一端关闭通信
  4. 调用close()后,调用方关闭通信,调用方的读和写缓冲区都关闭
  5. 调用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:数据传输协议中的代表协议。
  1. 默认0,根据type选择指定协议。
  2. 流式协议SOCK_STREAM代表协议TCP
  3. 报式协议SOCK_DGRAM代表协议UDP
  • 返回值
  1. 成功:新套接字对应的文件描述符
  2. 失败:-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
  1. addr要先初始化
  • addrlen:sizeof(addr),地址结构的大小
  • 返回值
  1. 成功:0
  2. 失败:-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
  • 返回值
  1. 成功:0
  2. 失败:-1 errno

accept函数
// 阻塞等待客户端建立连接,成功的话,返回一个与客户端成功连接的socket文件描述符 
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

  • sockfd:套接字对应的文件描述符
  • addr:传出参数。成功与服务器建立连接的客户端的地址结构(ip+port)
  • addrlen:传入传出参数。&client_addr_len
  1. 传入的是调用者提供的addr缓冲区的长度以避免缓冲区溢出问题
  2. 使用前需要先定义addr缓冲区的长度
  3. 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传NULL,表示不关心客户端的地址
  • 返回值
  1. 成功:能与服务器进行数据通信的客户端socket对应的文件描述符
  2. 失败:-1 errno

connect函数
// 客户端使用现有的socket与服务器建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

  • sockfd:客户端套接字对应的文件描述符
  • addr:传入参数。服务器的地址结构
  • addrlen:服务器的地址结构的大小
  • 返回值
  1. 0:成功
  2. -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:代表欲设置的网络层,
  1. SOL_SOCKET:socket层
  • optname:代表欲设置的选项参数
  1. SO_REUSEADDR:地址复用
  • optval:代表欲设置的值
  1. 1:启用
  2. 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
  1. 在多进程并发服务器中,每个connectfd都有一个子进程与之对应;
  2. 在多进程并发服务器中,由于支持多个客户端与服务器建立连接,所以Accept应该循环执行,一直监听客户端的连接请求
  • fork():产生子进程
  • Close():fork产生子进程后,子进程和父进程一样,都有listenfd和connectfd两个套接字。子进程不需要监听套接字,只需要与客户端通信的套接字,所以关闭listenfd;父进程不需要与客户端通信的套接字,只需要监听套接字用来监听客户端请求,所以关闭connectfd
  • read():读缓冲
  • 将读到的字节进行操作
  • write():写缓冲
  • signal():信号捕捉,如果捕捉到SIGCHLD信号,说明子进程结束,就调用处理函数,完成子进程回收
  • waitpid:因为waitpid()可以设置不阻塞,所以使用waitpid()。
  1. 由于设置waitpid()为非阻塞,所以当子进程没结束时,返回0,需要再次执行去回收,防止所有子进程都变为僵尸进程,所以需要设置为循环执行waitpid(),子进程一旦结束,一定要立刻回收。但循环执行的话,父进程可能会阻塞于waitpid(),此时如果有新的客户端连接请求,父进程无法响应。
  2. 结局办法:将上述操作放入信号处理函数中
  • 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:传入传出参数,读文件描述符的监听集合,只监听该集合中文件描述符的读事件
  1. 传入传出参数:传入的是想要监听的文件描述符,函数执行完,传出的是满足对应事件的文件描述符
  • writeset:传入传出参数,写文件描述符的监听集合,只监听该集合中文件描述符的写事件
  1. NULL:不监听文件描述符的写事件
  • exceptset:传入传出参数,异常文件描述符的监听集合,只监听该集合中文件描述符的异常事件
  1. NULL:不监听文件描述符的异常事件
  • timeout:阻塞监听时间
  1. NULL:阻塞,一直等
  2. timeval:timeval是一个结构体,有两个成员变量:tv_sec和tv_usec,分别是秒和微秒
  3. >0:设置成员变量为固定时间
  4. 0:不阻塞,检查描述字后立即返回,轮询
  5. 返回值:
  6. >0:所有的文件描述符监听集合中满足对应事件的总个数
  7. 0:没有满足对应事件的文件描述符
  8. -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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C++编程指南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值