linux 网络编程 基础

 网络编程基础

大端小端:

 

字节序,顾名思义,指字节在内存中存储的顺序。比如一个int32_t类型的数值占用4个字节,这4个字节在内存中的排列顺序就是字节序。字节序有两种:

(1)小端字节序(Little endinan),数值低位存储在内存的低地址,高位存储在内存的高地址;

(2)大端字节序(Big endian),数值高位存储在内存的低地址,低位存储在内存的高地址。

网络字节顺序采用big endian排序方式。

  A load word or store word instruction uses only one memory address. The lowest address of the four bytes is used for the address of a block of four contiguous bytes.

How is a 32-bit pattern held in the four bytes of memory? There are 32 bits in the four bytes and 32 bits in the pattern, but a choice has to be made about which byte of memory gets what part of the pattern. There are two ways that computers commonly do this:

Big Endian Byte Order: The most significant byte (the "big end") of the data is placed at the byte with the lowest address. The rest of the data is placed in order in the next three bytes in memory.

Little Endian Byte Order: The least significant byte (the "little end") of the data is placed at the byte with the lowest address. The rest of the data is placed in order in the next three bytes in memory.

  In these definitions, the data, a 32-bit pattern, is regarded as a 32-bit unsigned integer. The "most significant" byte is the one for the largest powers of two: 231, ..., 224. The "least significant" byte is the one for the smallest powers of two: 27, ..., 20.

For example, say that the 32-bit pattern 0x12345678 is at address 0x00400000. The most significant byte is 0x12; the least significant is 0x78. Here are the two byte orders:

Within a byte the order of the bits is the same for all computers (no matter how the bytes themselves are arranged).

 

  • 套接字编程需要指定套接字地址作为参数,不同的协议族有不同的地址结构,比如以太网其结构为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

 

 
/* 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的关

第二章:TCP网络编程流程

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

  • 创建网路套接字接口函数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.
  •  

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).
  •  

int protocol

  • TCP is always selected for the SOCK_STREAM socket type, and UDP is always used as the transport protocol for  SOCK_DGRAM
  •  

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

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

int listen(int sockfd, int backlog)

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

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

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

 

数据的IO和复用

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

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

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

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

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

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

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

  • flags含义:

I/O模型

I/O复用

 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:检查描述符后立即返回(轮询)

非阻塞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对象。
 

管道

#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 消息队列 信号量 共享内存

 

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

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

 

 

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

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

 

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

 

 

 

经验:

  • 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中

 

posted @ 2019-06-18 18:01  codestacklinuxer  阅读(2868)  评论(0编辑  收藏  举报