Linux下的TCP/IP编程----基础篇

时间:2021-04-11 10:16:39

Socket
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作”套接字”,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原意那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务

Socket的链接步骤:

服务端:

创建套接字—-绑定端口地址—-监听链接请求—-接受链接请求——进行通讯

1. int socket(int domain,int type, int protocol):创建socket套接字

  • domain (协议族):该参数决定socket使用哪种协议进行通讯

    PF_INET——IPv4协议族
    PF_INET6——-IPv6协议族
    PF_LOCAL——本地通讯的Unix协议族
    PF_PACKET——底层套接字协议族
    PF_IPX——IPX Novell协议族

  • type (套接字类型):该参数决定了socket在协议族的限定下,使用那种方式进行连接

    SOCK_STREAM(面向连接的):
    这种连接方式是要在两个socket端建立一个长连接,在通许期间是不断开的,始终保持连接。所以其在传输过程中数据不会消失,所有的数据都是按照顺序传输,而且传输的数据没有边界限定。

    SOCK_DGRAM(面向消息的):
    这种连接方式在通讯时不需要始终维持一个连接,只需要在进行通讯时将数据进行包裹后向目的socket发出即可。这种连接方式并不是按照数据的顺序依次进行传输,而是把数据分割包装在包裹中,所以数据的可能会发生丢失或者是损毁,而且每次传输限制数据的大小(不能超过每个数据包的大小),所以每次传输是有边界的。

  • protocol (协议):该参数决定了socket在协议族和连接方式的限定下具体使用哪种协议进行通讯

    根据具体选择的协议族和连接方式进行选择,当所选的协议族和连接类型的限定下只有一种协议时,可以传入0作为默认值。

socket()函数成功时返回文件描述符,失败时返回-1

2. int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen):为socket分配(绑定)地址

  • sockfd(socket的文件描述符):服务端的socket描述符

  • myaddr(socket地址):服务端的socket地址

  • addrlen(socket地址长度):服务端的socket地址长度

若成功绑定则返回0,失败则返回-1

3.int listen(int sockfd, int backlog):监听连接请求

  • sockfd(socket的文件描述符):服务端的socket描述符

  • backlog(连接请求队列的长度):表示最多可进入队列的连接请求数量。

若进入监听状态成功则返回0,失败时返回-1

4. int accept(int sockfd, struct sockaddr addr, socklen_t addrlen):接受连接请求,进行通讯

  • sockfd(socket的文件描述符):服务器端描述符

  • addr (socket地址):保存发起连接请求的socket客户端地址信息

  • addrlen(socket地址长度) :客户端socket地址长度

若成功则返回文件描述符(即将要通讯的客户端的socket描述符),失败则返回-1

5. 至此就完成了服务端的建立,在当客户端进行连接请求时就可以接受连接,进行和通讯。

客户端:

创建套接字—-请求链接服务器—–进行通讯

1.int connect(int sockfd, struct sockaddr * serv_addr, socklen_t addrlen ):客户端请求连接

  • sockfd(socket文件描述符):要连接的服务端 socket描述符

  • serv_addr(socket地址):要连接的服务端的地址

  • addrlen(socket地址长度):服务端的socket地址长度

若成功则返回0,失败则返回-1

2. 至此就完成了客户端的建立,只要在请求连接成功之后便可以和服务端进行通讯

客户端/服务端的通讯:

因为在Linux中所有的一切都被视为是文件,所以socket之间的读写也被认为是文件读写,所以就需要了解Linux中的文件操作。

分配给系统的标准输入/输出/错误文件描述符

文件描述符 对象
0 标准输入:Standard Input
1 标准输出:Standard Output
2 标准错误:Standard Error

int open(const *path, int flag):打开文件

  • path(文件名的字符串地址):要打开的文件名的字符串地址

  • flag(文件打开模式信息):文件的打开方式

    O_CREAT——必要时创建文件

    O_TRUNC——删除全部现有文件

    O_APPEND——维持现有文件,保存到其后面

    O_RDONLY——只读打开

    O_WRONLY——只写打开

    O_RDWR——读写打开
    成功时返回文件描述符,失败时返回-1

int close(int fd):关闭文件

  • fd(文件描述符):要关闭的文件描述符

成功时返回0,失败时返回-1

ssize_t write(int fd, const void *buf, size_t nbytes):向文件中写入数据

  • fd(文件描述符):要写入的对象的文件描述符

  • buf(缓冲指针):保存要写入数据的缓存地址的指针

  • nbytes(缓冲数据的字节数):要传送数据的字节数

成功则返回写入的字节数,失败则返回-1

ssize_t read(int fd, const void *buf, size_t nbytes):从文件中读取数据

  • fd(文件描述符):要读入的对象的文件描述符

  • buf(缓冲指针):保存要读入数据的缓存地址的指针

  • nbytes(读入数据的字节数):要接受数据的最大字节数

成功时返回读取的字节数(若遇到文件末尾则返回0),失败时返回-1

ssize_t send(int fd, const void *buf, size_t nbytes,int flag):写出数据

  • fd(文件描述符):要写入的对象的套接字文件描述符

  • buf(缓冲指针):保存要写入数据的缓存地址的指针

  • nbytes(缓冲数据的字节数):要传送数据的字节数

  • flag(标志位):传输数据时指定的可选信息项

成功时返回发送的字节数,失败时返回-1

ssize_t recv(int fd, const void *buf, size_t nbytes):从文件中读取数据

  • fd(文件描述符):要读入的对象的套接字文件描述符

  • buf(缓冲指针):保存要读入数据的缓存地址的指针

  • nbytes(读入数据的字节数):可接收的最大字节数

  • flag(标志位):接收数据时的可选信息项
    成功时返回接受的字节数(收到EOF时返回 0),失败时返回-1

ssize_t readv(int fds, const struct iovec* iov, int iovcnt):从文件中读取数据

  • fds(文件描述符):表示数据传输对象的套接字文件描述符,也可以像write函数一样传入文件或者是标准输入描述符

  • iov(结构体数据指针):iovec结构体数组的地址,结构体中包含发送数据的位置和大小信息。

  • iovcnt(数组大小):向第二个参数传递的数字长度

ssize_t writev(int fds, const struct iovec* iov, int iovcnt):向文件中写入数据

  • fds(文件描述符):表示数据传输对象的套接字文件描述符,也可以像read函数一样传入文件或者是标准输出描述符

  • iov(结构体数据指针):iovec结构体数组的地址,结构体中包含发送数据的位置和大小信息。

  • iovcnt(数组大小):向第二个参数传递的数字长度

read&write, send&recv,readv&writev异同:

相同点:都可以进行基本的数据的读写操作,只从数据的传输结果来看没有太大的区别。

不同点:

read&write函数是标准的文件操作函数,只能进行文件的读写,并根据函数的返回值来判断读写的情况,其使用简单方便,但是只能满足基本的读写需求。

send&recv函数则在在read&write函数的基础上进行了扩充,多添加了一个标志位,使得在传输数据的同事能传递一些控制信息,增强了文件读写的可控制性。

readv&writev函数则是可以对数据进行整合来进行能够传输和发送,通过将多个缓冲区的数据进行整合发送,减少I/O函数调用的次数,提高了传输的效率。

地址族与数据序列:

我们都知道在网络中是通过IP地址来确定一个主机在网络中的位置,因而可以想到在网络通讯中也是在这个基础之上进行的。准确的来说,socket通信中主要是通过IP地址+端口号的方式来进行程序间的通讯:通过IP地址来唯一确定网络上的一台主机,通过端口号来唯一确定主机上的一个通讯程序。

IP地址:
IP地址分为IPv4和IPv6,目前只涉及到IPv4。两者的主要区别在于用于表示地址的字节长度不同,IPv4用4字节(32bit)来表示一个地址,IPv6用16字节(128bit)来表示一个地址。

IPv4地址分类:

A类:0~127
B类:128~191
C类:192~223

端口号:
端口号用于区分在 同一主机三个的多个应用进程,由16bit来表示,所以范围是0~65535,但是0~1023是常用的熟知端口(如web服务是80端口,FTP服务是21端口),所以仅可以使用剩下的端口号。

数据序列:

由于在不同的cpu中对于数字的保存方式不同,例如对于4字节整数型值1,有的cpu保存方式为 00000000 00000000 00000000 00000001 ,而有的cpu则是倒序保存  00000000 00000001 00000000 00000000 这也就是我们所说的大端模式(高位字节存放在地位地址)和小端模式(高位字节存放在高位地址),该处的地址称为主机字节序。

为了在网络通讯时方便,统一规定网络字节序为大端模式,所以在进行地址的设定时要先进行地址的转换。

unsigned short htons(unsigned short):主机字节序转换成网络字节序,用于转换端口号

  • short(要转换的数据):要转换的数据
    返回网络字节序

unsigned short ntohs(unsigned short):网络字节序转换成主机字节序,用于转换端口号

  • short(要转换的数据):要转换的数据
    返回网络字节序

unsigned long htonl(unsigned long):主机字节序转换成网络字节序,用于转换IP地址

  • long(要转换的数据):要转换的数据
    返回网络字节序

注:s代表short,l代表long,h代表主机字节序,n代表网络字节序。


unsigned long ntohl(unsigned long):网络字节序转换成主机字节序,用于转换IP地址

  • long(要转换的数据):要转换的数据
    返回网络字节序

in_addr_t inet_addr(const char * string):将传入的IP地址转换成32位整型数,并转换成网络字节序

  • string (字符串地址):要转换的点分十进制的IP地址字符串,例如192.168.10.1

成功时返回32位打断序整数型值,失败时返回INADDR_NONE

在网络通讯中要使用socket_addr这个结构体来保存socket地址,因此需要对这个结构体有所了解。

struct sockaddr{
sa_family_t sin_family;//地址族
char sa_data[14];
}

struct sockaddr_in{
sa_family_t sin_family;//地址族
uint16_t sin_port;//16位的TCP/IP端口号(网络字节序)
struct in_addr sin_addr;//32位的IPv4地址(网络字节序)
char sin_zero[8]//不使用(必须填充位0,只是为了和sockaddr结构保持一致)
}

struct in_addr{
in_addr_t s_addr;//32位IPv4地址
}