《网络编程 — 常用API介绍》

 1.创建套接字

socket函数
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
第一个参数domain:指定协议族
  • AF_UNIX: 文件系统套接字(通过UNIX和linux文件系统实现的本地套接字)
  • AF_INET: UNIX网络套接字(用于通过包括因特网在内的TCP/IP网络进行通信的程序)
  • AF_ISO:      ISO标准协议
  • AF_NS: 施乐系统网络系统协议
  • AF_IPX: Novell IPX协议
  • AF_APPLETALK: Appletalk DDS
第二个参数type:指定这个套接字的通信类型
  • SOCK_STREAM:是一个有序、可靠、面向连接的双向字节流,与管道类似。所以数据收发之前需要连接。
  • SOCK_DGRAM:固定长度的、无连接的、不可靠的报文传递。
  • SOCK_RAW:IP协议的数据报接口(在posix.1中为可选)
  • SOCK_SEQPACKET:固定长度的、有序的、可靠的、面向连接的报文传递
第三个参数protocol:指定使用的协议,设置为0表示使用默认协议。
  例如:在AF_INET通信域中,套接字类型SOCK_STREAM的默认协议是传输控制协议(TCP)。而如果套接字类型SOCK_DGRAM的默认协议是UDP。
  也可以指定参数:
  • IPPROTO_TCP:传输控制协议
  • IPPROTO_UDP:用户数据报协议
  • IPPROTO_IP:IPv4网际协议
  • IPPROTO_IPV6:IPv6网际协议
  • IPPROTO_ICMP:因特网控制报文协议
  • IPPROTO_RAM:原始IP数据包协议

 返回值:

  成功返回套接字文件描述符,失败返回-1

  作用:系统调用创建一个套接字并返回一个描述符,该描述符可以用来访问该套接字。

  《网络编程 — 常用API介绍》

2.寻址

2.1网络地址与端口号 

  进程表示由两部分组成:计算机的网络地址和端口号。

  网络地址:帮助标识网络上我们想通信的计算机

  端口号:帮助标识特定的进程。

2.2字节序

  简单来说,就是指的超过一个字节的数据类型在内存中存储的顺序(也就是说0x10,这种不存在大端或小端的纠结

  计算机中存储数据分为两种:大端字节序(big endian)小端字节序(little endian)

   小端字节序:低字节存于内存低地址;高字节存于内存高地址;

  long型数据0x12345678在小端系统中:

  《网络编程 — 常用API介绍》

  内存的地址是由低到高的顺序;而数据的字节也是由低到高的。

  大端字节序:高字节存于内存低地址;低字节存于内存高地址;

  long型数据0x12345678在大端系统中:

   《网络编程 — 常用API介绍》

  内存的地址是由低到高的顺序;而数据的字节却是由高到低的 

问题:既然大端字节序是我们常用的,那为什么还有小端字节序?

  因为计算机电路先处理低位字节,效率比较高,计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。但是,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

   计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读第一个字节,再读第二个字节。

问题:怎么判断自己的电脑是大端还是小端?

#include <stdio.h>
#include <stdlib.h>
/**
 * 联合类型的变量类型,用于测试字节序
 * 成员value的高低端字节可以由成员byte按字节访问
 *
 * */
typedef union{
    unsigned short int value;
    unsigned char byte[2];
}to;

int main(int argc, char *argv[])
{
    to typeorder;
    typeorder.value = 0xabcd;

    if(typeorder.byte[0] == 0xcd&& typeorder.byte[1] == 0xab){
        printf("Low endian byte order"
                "byte[0]:0x%x,byte[1]:0x%x
",
                typeorder.byte[0],
                typeorder.byte[1]);
    }else if(typeorder.byte[0] == 0xab&& typeorder.byte[1] == 0xcd){
        printf("High endian byte order"
                "byte[0]:0x%x,byte[1]:0x%x
",
                typeorder.byte[0],
                typeorder.byte[1]);
    }
    return 0;
}

  注意:联合体是共享一块内存的。

网络字节序

   网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理。UDP/TCP/IP协议规定网络字节序就是大端字节序。

  所以说,网络字节序是大端字节序;比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值;

  以下4个函数用来处理器字节序和网络字节序之间实施转换的函数。

uint32_t htonl(uint32_t hostint32);

uint16_t htons(uint16_t hostint16);

uint32_t ntohl(uint32_t netint32);

uint16_t ntohs(uint16_t netint16)

3.套接字地址结构

3.1通用套接字数据结构

  套接字编程需要指定套接字的地址作为参数,不同的协议族有不同的地址结构定义方式。例如以太网,其结构名称为sockaddr_in。

  sockaddr为通用套接字数据结构,它可以在不同协议族之间进行强制转换。可以认为其他的地址结构都是由它进行细分出来的。

struct sockaddr{                    /* 套接字地址结构 */
    sa_family_t sun_family;         /* 协议族 */
    char            sa_data[14];    /* 协议族数据 */
};            
typedef unsigned short sa_family_t; /* 16位 */

  

3.2以太网常用的地址结构sockaddr_in

struct sockaddr_in{
   short int                 sin_family;    /* 协议族,以太网一般位AF_INET */
  unsigned short int        sin_port;      /* 16位的端口号,网络字节序 */
  struct in_addr           sin_addr;       /* IP地址32位 */
   char                    sin_zero[8];    /* 保留 */
};

  由于结构struct sockaddr和结构struct sockaddr_in的大小是完全一致的,所以进行地址结构设置时,通常的方法是利用结构struct sockaddr_in进行设置,然后强制转换为结构struct sockaddr类型。因为这两个结构大小是完全一致的,所以进行这样的转换不会有副作用。

  IP地址结构in_addr被定义为:

  struct in_addr{

  unsigned long int         s_addr;

  };

  IP地址中的4个字节组成一个32位的值。一个AF_INET套接字由它的域、IP地址和端口号来完全确定。从应用程序的角度来看,所有套接字的行为就像文件描述符一样,

并且通过一个唯一的整数值来区分。

3.命名套接字

 bind函数

#include <sys/socket.h>
#include <sys/types.h>
int bind(int socket, const struct sockaddr *address, size_t address_len);
  • 第一个参数socket:由socket()函数创建得到得套接字描述符
  • 第二个参数address:我们指定要绑定的域、IP地址和端口的sockaddr结构体
  • 第三个参数address_len:sockaddr结构体的长度。通常使用sizeof(struct sockaddr)

返回值:0表示绑定成功,-1表示绑定失败。可以通过error查看错误值

   作用:bind系统调用把参数address中的地址分配给与文件描述符socket关联。

  《网络编程 — 常用API介绍》

4.创建套接字队列

listen函数

#include <sys/socket.h>
#include <sys/types.h>
int listen(int socket, int backlog);
第二个参数backlog:等待处理的进入连接的个数最多不能超过这个数字
返回值: 成功:0; 失败:-1;

   作用:初始化服务器可连接队列,服务器处理客户端连接请求的时候是顺序处理的,同一时间仅能处理一个客户端连接。当多个客户端的连接请求同时到来的时候,服务器并不是同时处理,而是将不能处理的客户端连接请求放到等待队列中,这个队列的长度由listen()函数来定义。

  《网络编程 — 常用API介绍》

5.接收连接

accept函数

#include <sys/socket.h>
#include <sys/types.h> int accept(int socket, struct sockaddr *address, size_t *address_len);
  参数:当accept()函数返回的时候,会将客户端的信息存储在参数address中。参数address_len表示第二个参数所指内容的长度,可以使用sizeof(struct sockaddr_in)来获得。
  需要注意的是:在accept中address_len参数是一个指针而不是结构,accept()函数将这个指针传给TCP/IP协议栈。
  返回值:成功返回新的描述符用于和客户端进行通信。失败返回-1。

  作用:只有当有客户程序试图连接到由socket参数指定的套接字上时才返回。这里的客户是指,在套接字队列中排在第一个的未处理连接。accpet函数将创建一个新的套接字来与该客户进行通信,并且返回新套接字的描述符。

   这样就会有两个描述符,老的文件描述符表示正在监听的socket,新产生的文件描述符表示客户端的连接,函数send()和recv()通过新的描述符进行数据收发。

  《网络编程 — 常用API介绍》

6.请求连接

connect函数

#include <sys/socket.h>
int connect(int socket, const struct sockaddr *address, size_t address_len);
参数:
  第一个参数:建立套接字的时候返回的描述符
  第二个参数:一个指向数据结构sockaddr的指针,其中包括客户端需要连接的服务器的目的端口和ip地址,以及协议类型。
  第三个参数:表示第二个参数内容的大小,可以使用sizeof(struct sockaddr)获得,与bind函数不同,这个参数是一个整形的变量而不是指针。
返回值: 成功:0; 失败:-1

  作用:参数socket指定的套接字将连接到参数address指定的服务器套接字,address指向的结构的长度由参数address_len指定。

  《网络编程 — 常用API介绍》

7.写入数据函数

write函数

int size 
char data[1024];
size = write(socket, data, 1024);    //将缓冲区data的数据全部写入套接字描述符socket中,返回值为成功写入的数据长度。
  注意:如果用tcp,socket是accept函数返回的套接字描述符。

  套接字描述符和普通的文件描述符是一样的,因此也可以用write和read进行操作。

send函数 

#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int s, const void *buf, size_t len, int flags);
参数:
  和recv的参数基本一致。
返回值:成功发送的字节数。发生错误的返回-1,可以查看errno获取错误码。

  由于用户缓冲区buf中的数据在通过send()函数进行发送的时候,并不一定能够全部发送,所以要检查send()函数的返回值,按照与计划发送的字节长度len是否相等来判断如何进行下一步操作。

  当send()函数的返回值小于len的时候,表明缓冲区中仍然有部分数据没有成功发送,这时需要重新发送剩余部分的数据。通常的剩余数据发送方法是对原来buf中的数据位置进行偏移,偏移的大小为已发送成功的字节数。

《网络编程 — 常用API介绍》

writev函数 

#include <sys/uio.h>
ssize_t writev(int fd, const srtuct iovec *vector, int count);

struct iovec{
  void *iov_base;    /* 向量的缓冲区地址 */
  size_t iov_len;    /* 向量缓冲区的大小,以字节为单位 */
};

作用:
  向套接字描述符s中写入在向量vector中保存的count块数据。
返回值:成功返回发送的字节数。错误返回-1,可以查看errno获取错误码。

《网络编程 — 常用API介绍》

sendmsg函数

比较复杂,用到再看 

8.读取数据函数

read函数

int size;
char data[1024];
size = read(socket, data, 1024);    //从套接字描述符socket中读取1024个字节,放入缓冲区data中,返回值是成功读取的数据大小

recv函数 

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int s, void *buf, size_t len, int flags);
参数:
  s:由socket函数得到得套接字描述符。
  buf:从套接字s中接收数据放在缓冲区buf中。
  len:buf的长度
  flag:用于设置接收数据的方式
返回值:
  错误返回-1,可以查看errno。成功返回接收的字节数。

《网络编程 — 常用API介绍》

 《网络编程 — 常用API介绍》

   recv()函数通常用于TCP类型,UDP使用recvfrom()函数接收数据,当然在数据报套接字绑定地址和端口后,也可以使用recv()函数接收数据。

readv函数

#include <sys/uio.h>
ssize_t readv(int s, const struct iovec *vector, int count);

struct iovec{
  void *iov_base;      /* 向量的缓冲区地址 */
  size_t iov_len;      /* 向量缓冲区的大小,以字节为单位 */
};

作用:
  从套接字描述符s中读取count块数据放在缓冲区向量vector中。
返回值:成功接收到的字节数。错误返回-1,查看errno获取错误码。

《网络编程 — 常用API介绍》

recvmsg函数

比较复杂,用到再看

9.关闭套接字函数

close函数和shutdown函数

#include <sys/socket.h>

int shutdown(int socket, int how);
参数:
  第一个参数是要切换通信的套接字描述符。
  第二个参数表示切断的方式。
  SHUT_RD:值为0,表示切断读。
  SHUT_WR:值为1,表示切断写。
  SHUT_RDWR:值为2,表示切换读写。
返回值:成功返回0,失败返回-1
或者 close(socket);

  close函数:关闭已经打开的socket连接,内核会释放相关的资源,关闭套接字之后就不能再使用这个套接字文件描述符进行读写操作。

  shutdown函数:允许单方向切断通信或者切断双方的通信。

  《网络编程 — 常用API介绍》

用户层和内核层交互过程

1.向内核传入数据的交互过程

  向内核输入数据的函数有send()、bind()等。传入过程如图:

  《网络编程 — 常用API介绍》

  bind()函数向内核中传入的参数有套接字地址结构结构的长度两个与地址结构有关的参数。

  地址结构通过内核复制的方式将其中的内容复制到内核。

  地址结构的长度通过传值的方式传入内核。

  内核按照用户传入的地址结构长度来复制套接字地址结构的内容。

2.内核传出数据的交互过程

  传出的函数有accept() ecv()等,过程如图:

  《网络编程 — 常用API介绍》

   通过地址结构的长度和套接字地址结构指针来进行地址结构参数的传出操作。

  传出过程和传入过程中参数不同的是,地址结构长度的参数在传入过程中是传值,而传出过程是通过传址完成的。内核按照用户传入的地址结构长度进行套接字地址结构数据的复制,将内核中的地址结构数据复制到用户传入的地址结构指针中。