linux 网络编程 基础

网络编程基础

  • 套接字编程需要指定套接字地址作为参数,不同的协议族有不同的地址结构,比如以太网其结构为sockaddr_in。
  • 通用套接字:
    struct sockaddr {
    sa_family_t sa_family; /* address family, AF_xxx 16Bytes */
    char sa_data[14]; /* 14 bytes of protocol address */
    };
  • 实际使用的套接字结构

  • 以bind函数为例:

    bind(int  sockfd, //套接字文件描述符

       struct sockaddr *uaddr,//套接字结构地址

       int addr_len)//套接字地址结构长度  

使用struct    sockaddr  为通用结构体,在以太网中,一般使用结构 sockaddr_in

  • 以太网套接字
/* Internet address. */
struct in_addr {
__be32 s_addr;
};
/* Structure describing an Internet (IP) socket address. */

#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */ /* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
  • 结构 sockaddr 和结构 sockaddr_in的关linux 网络编程 基础

第二章:TCP网络编程流程

tcp网络编程主要采取C/S模式,即客户端(C)、服务器(S)模式

linux 网络编程 基础

  • 创建网路套接字接口函数socket

int socket (int family, int type, int protocol)

int family

  • AF_UNIX : Sockets for interprocess communication in the local computer.
  • AF_INET : Sockets of the TCP/IP protocol family based on the Internet Protocol Version 4
  • AF_INET6 : TCP/IP protocol family based on the new Internet Protocol, Version 6.
  • AF_IPX : IPX protocol family.
  • linux 网络编程 基础

int type

  • SOCK_STREAM (stream socket) specifies a stream-oriented, reliable, in-order full duplex connection between two sockets.
  • SOCK_DGRAM (datagram socket) specifies a connectionless, unreliable datagram service, where packets may be transported out of order.
  • SOCK_RAW (raw socket).
  • linux 网络编程 基础

int protocol

  • TCP is always selected for the SOCK_STREAM socket type, and UDP is always used as the transport protocol for  SOCK_DGRAM
  • linux 网络编程 基础
  • linux 网络编程 基础

int bind(int sockfd,  struct sockaddr *uaddr,  socketlen_t  uaddrlen)

  • sockfd为 socket()函数创建返回的fd
  • uaddr 指向一个包含了ip地址 端口等信息
  • uaddrlen 是sockaddr的长度
  • bind 可以指定Ip地址或者端口  可以都指定
  • linux 网络编程 基础

    linux 网络编程 基础

int listen(int sockfd, int backlog)

  • sockf为socket创建成功返回的fd
  • backlog 表示在accept 函数处理之前在等待队列中允许最多的客户端个数
  • linux 网络编程 基础

int accept(int sockfd,  struct sockaddr *addr,  socketlen_t * addrlen)

  • accept 函数可以得到成功连接客户端的ip地址、端口信息和协议族等信息
  • accpet返回值是新连接客户端套接字的描述符

linux 网络编程 基础

数据的IO和复用

常用的数据I/O函数有recv/send()  readv/writev  recvmsg/sendmsg

int recv(int sockfd, void *buf, size_t len, int flag)

  • recv 函数的参数flag用于设置接收数据的方式
  • recv函数返回成功接收到的字节数,错误时返回-1

linux 网络编程 基础

int send(int sockfd,  const void *buf,  size_t len,  int flags)

  • send函数成功发送的字节数,发生错误是返回-1

linux 网络编程 基础

int recvmsg (int sockfd, struct msghdr *msg, int flags)

  • recvmsg 表示从sockd 中接收数据放在缓冲区,其操作方式由flags指定
  • 其返回值表示成功接收到的字节数,-1时表示发生错误
  • 当对端使用正常方式关闭连接时,返回值为0,如调用close

linux 网络编程 基础

linux 网络编程 基础

  • flags含义:

linux 网络编程 基础

I/O模型

linux 网络编程 基础

linux 网络编程 基础

I/O复用

linux 网络编程 基础

select函数简介

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

  • maxfdp1:指定待测试的描述符个数,它的值是待测试的最大描述符加1
  • readset、writeset、exceptset:指定让内核测试读、写、异常条件的描述符
  • timeout:最长等待时间
  • timeout参数的三种可能:
    a.设为空指针:永远等待下去,仅在有描述符就绪时才返回
    b.正常设置timeout,在不超过timeout设置的时间内,在有描述符就绪时返回
    c.将timeout.tv_sec和timeout.tv_usec都设为0:检查描述符后立即返回(轮询)

linux 网络编程 基础

非阻塞I/O

  • 非阻塞connect 以及非阻塞accept

  • 以及调用select 的非阻塞I/O

进程间通信

  • Unix域协议

  

#define UNIX_PATH_MAX 128
struct sockaddr_un{
sa_family_t sun_family; /* AF_UNIX 或者 AF_LOCAL */
char sun_path[UNIX_PATH_MAX]; /* path name */
};
使用流程分析:
一、服务器端通信过程分析
服务器端基本遵循面向连接的socket数据流通信过程。
1、调用socket()函数,建立socket对象,指定通信协议为AF_UNIX。
2、调用bind()函数,将创建的socket对象与bind()函数产生的那个socket类型的文件server_socket P绑定。
UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,其地址用结构体sockaddr_un表示, 网络编程的socket地址是IP地址加端口号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径,这个socket文件由bind()调用创建,如果调用bind()时该文件已存在,且已被link,则bind()错误返回。一个套接字只能绑定到一个路径上,同样的,一个路径也只能被一个套接字绑定。
sockaddr_un结构的sun_path成员包含一路径名,当我们将一地址绑定至UNIX域套接字时,系统用该路径名创建一类型为S_IFSOCK的文件。该文件仅用于向客户端进程告知套接字名字,该文件不能打开,也不能由应用程序用于通信,当关闭套接字时,并不自动删除该文件,所以我们必须确保在应用程序终止前,对该文件执行解除链接操作(unlink(path)),或删除该文件。
struct sockaddr_un结构有两个参数:sun_family、sun_path。sun_family只能是AF_LOCAL或AF_UNIX;而sun_path就是本地文件的路径。存放文件路径的sun_path数组必须以空字符(即’\0’字符)结尾
3、调用listen()函数,使socket对象处于监听状态,并设置监听队列大小。
4、服务器端监听到该请求,在客户端发出请求后,accept()函数接收请求,返回新文件描述符,从而建立连接。
5、服务器端调用read()函数接收数据(开始处于阻塞状态,等待客户端发送数据,因此,客户端在编程是需要首先发送数据,接收到数据后,输出接收到的数据)。
6、调用write()函数发送数据到客户端。
7、通信完成后,调用close()函数关闭socket对象;unlink(sockaddr_un.sun_path)。
 
二、客户端通信过程分析
客户端基本遵循面向连接的socket数据流通信过程。
1、调用socket()函数,建立socket对象,指定相同通信协议。
2、客户端调用connect()函数,向服务器端发起连接请求。
3、在得到服务器端允许后,首先调用write()函数向服务器端发送消息(因服务器端循环体中首先是接收数据)。
4、调用read()函数接收数据。
5、通信完成后,调用close()函数关闭socket对象。
 

管道

linux 网络编程 基础

#include <unistd.h>
int pipe(int pipefd[2]);

成功调用 pipe 函数之后,可以对写入端描述符 pipefd[1] 调用 write ,向管道里面写入数据,比如

write(pipefd[1],wbuf,count);

一旦向管道的写入端写入数据后,就可以对读取端描述符 pipefd[0] 调用 read

管道有如下三条性质:
· 只有当所有的写入端描述符都已关闭,且管道中的数据都被读出,对读取端描述符调用 read 函数
才会返回 0 (即读到 EOF 标志)。
· 如果所有读取端描述符都已关闭,此时进程再次往管道里面写入数据,写操作会失败, errno 被设
置为 EPIPE ,同时内核会向写入进程发送一个 SIGPIPE 的信号。
· 当所有的读取端和写入端都关闭后,管道才能被销毁

这种管道因为没有实体文件与之关联,适用于有亲缘关系的任意两个进程之间通信

命名管道 FIFO

命名管道就是为了解决无名管道的这个问题而引入的。 FIFO 与管道类似,最大的差别就是有实体
文件与之关联。由于存在实体文件,不相关的没有亲缘关系的进程也可以通过使用 FIFO 来实现进程之
间的通信;

从外表看,我是一个 FIFO 文件,有文件名,任何进程通过文件名都可以打开我

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

一旦 FIFO 文件创建好了,就可以把它用于进程间的通信了。一般的文件操作函数如 open 、 read 、 write 、 close 、 unlink 等都可以用在 FIFO 文件
上; 对 FIFO 文件推荐的使用方法是,两个进程一个以只读模式( O_RDONLY )打开 FIFO 文件,另一个以只写模式( O_WRONLY )打开 FIFO 文
件。这样负责写入的进程写入 FIFO 的内容就可以被负责读取的进程读到,从而达到通信的目的

System V 消息队列 信号量 共享内存

linux 网络编程 基础

管道和 FIFO 都是字节流的模型,这种模型不存在记录边界,如果从管道里面读出 100
个字节,你无法确认这 100 个字节是单次写入的 100 字节,还是分 10 次每次 10 字节写入的,你也无法知
晓这 100 个字节是几个消息。管道或 FIFO 里的数据如何解读,完全取决于写入进程和读取进程之间的约
定;System V 消息队列是优于管道和 FIFO 的。原因是消息队列机
制中,双方是通过消息来通信的,无需花费精力从字节流中解析出完整的消息;

System V 消息队列比管道或 FIFO 优越的第二个地方在于每条消息都有 type 字段,消息的读取进程可
以通过 type 字段来选择自己感兴趣的消息,也可以根据 type 字段来实现按消息的优先级进行读取,而不
一定要按照消息生成的顺序来依次读取

一般来说,信号量是和某种预先定义的资源相关联的。信号量元素的值,表示与之关联的资源的个数

一旦将信号量和某种资源关联起来,就起到了同步使用某种资源的功效

共享内存是所有 IPC 手段中最快的一种。它之所以快是因为共享内存一旦映射到进程的地址空间,
进程之间数据的传递就不须要涉及内核了。
回顾一下前面已经讨论过的管道、 FIFO 和消息队列,任意两个进程之间想要交换信息,都必须通
过内核,内核在其中发挥了中转站的作用:
· 发送信息的一方,通过系统调用( write 或 msgsnd )将信息从用户层拷贝到内核层,由内核暂存这
部分信息。
· 提取信息的一方,通过系统调用( read 或 msgrcv )将信息从内核层提取到应用层

linux 网络编程 基础

linux 网络编程 基础

经验:

  • epoll 或者 select 处理事件时,可读事件时,read返回值-1,如果errno不为EAGAIN,可以认为失败,并关闭fd。read返回0,说明对方断开连接,此时也需要关闭fd。如果链路断了,如拔掉网线,需要是用keepalive来触发可写事件
  • 本地UDP发送过快也是会丢包的。非阻塞情况下的unix domain socket哪怕是STREAM的也是会丢包的
  • 使用unix socket通信相比于本地udp通信减少了校验和的计算。使用阻塞函数时,unix domain socket可以保证不丢包不乱序,但是当发送缓冲区满了的话则会阻塞。使用非阻塞操作时经测试会丢包
  • 使用setsockopt设置发送缓冲时,SO_RCVBUF和SO_SNDBUF的最大值受系统设置限制,可以使用SO_RCVBUFFORCE和SO_SNDBUFFORCE来无视系统设置
  • SIGPIPE信号,网络编程时一定要处理该信号。同样一般要设置的还有SO_REUSEADDR。当客户端close连接时,若server继续发送数据,会收到RST,继续写就会SIGPIPE
  • 网络编程对事件进行封装,提供注册回调函数,在可读、可写时进行函数调用。一般用法,针对非阻塞情况,初始化时将可读事件注册,需要写的时候先写,写不下去的时候(errno=EAGAIN)再挂上可写事件,只要发送缓冲区还有空间,就是可写的
  • 基于事件的编程框架,需要记录最后一次成功read或write的时间,如果idletime大于阈值,直接close
    • 服务器编程可以设置最大的fd个数,然后一次性申请FileEvent数组,之后由fd到事件查询代价O(1)
      • 针对非阻塞socket,connect返回EINPROGRESS时需要将fd加到可写事件监视集合中,当select()或者poll()返回可写事件时,需要用getsockopt去读SOL_SOCKET层面的SO_ERROR选项,SO_ERROR为0表示连接成功,否则为连接失败
      • epoll ET模式的处理方式。读:只要可读就一直读,一直读到返回0,或者error = EAGAIN。写:只要可写就一直写,知道数据发送完,或者errno = EAGAIN
      • socket read缓冲区最大值TCP可查看”/proc/sys/net/ipv4/tcp_rmem”, udp 65536
      • 实现定时器时通常办法是select/poll/epoll接口,精度毫秒级;还有就是新增的系统调用timerfd_create 把时间变成了一个文件描述符,该“文件”在定时器超时的那一刻变得可读,高于poll的精度
      • 在主动关闭连接时,可以先shutdown(fd, SHUT_WR)关闭写端,等对方close时再关闭读端。这样子的好处是如果对方已经发送了一些数据,这些数据不会漏收。这就要求对端在read返回0之后关闭连接或者shutdown写端
      • 网络编程一种比较好的模型是“one loop per thread”,如果事件库不是线程安全的,则需要使用pipe或者
      • socketpair通知,子线程接受到通知(fd可读)后处理,kernel 2.6.22加入了eventfd,是更好的通知方法
      • TCP Nagle算法和TCP Delayed Ack机制可能会导致网络延时(Linux 40ms, Windows 200ms),最容易产生问题的就是"Write-Write-Read”这种模型,发送端的Nagle算法和接收端的Delayed Ack会导致一直等到接收端delayed ack超时后数据才发送出去
      • accept返回EMFILE,进程描述符用完了,无法创建新的socket,也无法close连接,会导致不断通知该可读事件,程序busy loop,cpu 100%,解决方法是事先准备一个nullfd=open(“/dev/null”),close该fd,accept,close socket,然后再nullfd=open(“/dev/null”),缺点是该方法线程不安全,多线程accept可能导致nullfd用于新socket创建,然后又处于busy loop中
上一篇:服务器编程入门(4)Linux网络编程基础API


下一篇:进程和线程