Linux网络编程

TCP/IP可以看成是一种底层协议,不管是Windows还是Linux中都会实现这个协议,不过接口可以各自实现,我们在Linux中的接口就是socket。socket会实现和底层TCP/IP的对接。所以我们主要是用socket来进行编程。

socket介绍

​ socket编程是一门技术,它主要是在网络通信中经常用到

​ 既然是一门技术,由于现在是面向对象的编程,一些计算机行业的大神通过抽象的理念,在现实中通过反复的理论或者实际的推导,提出了抽象的一些通信协议,基于tcp/ip协议,提出大致的构想,一些泛型的程序大牛在这个协议的基础上,将这些抽象化的理念接口化,针对协议提出的每个理念,专门的编写制定的接口,与其协议一一对应,形成了现在的socket标准规范,然后将其接口封装成可以调用的接口,供开发者使用

​ 目前,开发者开发出了很多封装的类来完善socket编程,都是更加方便的实现刚开始socket通信的各个环节,所以我们首先必须了解socket的通信原理,只有从本质上理解socket的通信,才可能快速方便的理解socket的各个环节,才能从底层上真正的把握

TCP/IP协议

​ 要理解socket必须的得理解tcp/ip,它们之间好比送信的线路和驿站的作用,比如要建议送信驿站,必须得了解送信的各个细节。

​ TCP/IP协议不同于OSI的7个分层,它是根据这7个分层,将其重新划分,好比打扫卫生,本来有扫帚,垃圾斗,抹布,涂料,盆栽等就好比OSI的标准几个分层,tcp/ip根据用途和功能,将扫帚,垃圾斗放到粗略整理层,抹布涂料放到中度整理层,盆栽放到最终效果层。这里TCP/IP也对OSI的网络模型层进行了划分:大致如下:

OSI模型:

img

TCP/IP对应的网络模型

EGsRgI.md.png

TCP/IP协议参考模型把所有的TCP/IP系列协议归类到四个抽象层中

应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等

传输层:TCP,UDP

网络层:IP,ICMP,OSPF,EIGRP,IGMP

数据链路层:SLIP,CSLIP,PPP,MTU

想了解上面的东西,我找了一篇文章可以看看。

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务,看起来大概是这样子的

img

通过上面的图形,由于底一层的需要向高一层的提供服务,我们大致的理解应用程序需要传输层的tcp和网络层的ip协议提供服务,但是我们这章要分析的socket它是在tcpip协议的那一部分呢,就好比,我们的通讯线路已经有明确的规定,我们的驿站要设计在哪个地方一样

socket地位

​ 到目前为止,大致的了解了应用程序和tcpip协议的大致关系,我们只是知道socket编程是在tcp/IP上的网络编程,但是socket在上述的模型的什么位置呢。这个位置被一个天才的理论家或者是抽象的计算机大神提出并且安排出来

img

我们可以发现socket就在应用程序的传输层和应用层之间,设计了一个socket抽象层,传输层的底一层的服务提供给socket抽象层,socket抽象层再提供给应用层,问题又来了,应用层和socket抽象层之间和传输层,网络层之间如何通讯的呢,了解这个之前,我们还是回到原点

要想理解socket编程怎么通过socket关键词实现服务器和客户端通讯,必须得实现的了解tcp/ip是怎么通讯的,在这个的基础上在去理解socket的握手通讯

在tcp/ip协议中,tcp通过三次握手建立起一个tcp的链接,大致如下

第一次握手:客户端尝试连接服务器,向服务器发送syn包,客户端进入SYN_SEND状态等待服务器确认

第二次握手:服务器接收客户端syn包并确认,同时向客户端发送一个SYN包,即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手

​ 三次握手如下图:

EGydiQ.png

​ 根据tcp的三次握手,socket也定义了三次握手,也许是参考tcp的三次握手,一些计算机大神们画出了socket的三次握手的模型图

​ 模型图如下:

img

​ 在上面图的基础上,如果我们得到上面的图形,需要我们自己开发一些接口,来满足上面的通讯的三次握手,问题就出来了,我们会需要开发哪些函数

socket的一些接口函数原理

​ 通过上面的图,我们清楚,我们好比一些泛型的程序员,一些理论提供者提供给了我们上面的图形的理论,我们需要做的就是讲上面的图形的抽象化的东西具体化

​ 第一次握手:客户端需要发送一个syn j 包,试着去链接服务器端,于是客户端我们需要提供一个链接函数

​ 第二次握手:服务器端需要接收客户端发送过来的syn J+1 包,然后在发送ack包,所以我们需要有服务器端接受处理函数

​ 第三次握手:客户端的处理函数和服务器端的处理函数

​ 三次握手只是一个数据传输的过程,但是,我们传输前需要一些准备工作,比如将创建一个套接字,收集一些计算机的资源,将一些资源绑定套接字里面,以及接受和发送数据的函数等等,这些功能接口在一起构成了socket的编程

​ 下面大致的按照客户端和服务端将所需的函数详细的列举出来

imgimg

上面的两个图都概述了socket的通讯原理

socket编程相关API

函数名称函数简单描述附加说明
socket创造某种类型的套接字
bind将一个 socket绑定一个ip与端口的二元组上
listen将一个 socket 变为侦听状态
connect试图建立一个 TCP 连接一般用于客户端
accept尝试接收一个连接一般用于客户端
send通过一个socket发送数据
recv通过一个socket收取数据
select判断一组socket上的读事件
gethostbyname通过域名获取机器地址
close关闭一个套接字,回收该 socket 对应的资源Windows 系统中对应的是 closesocket
shutdown关闭 socket 收或发通道
setsockopt置一个套接字选项
getsockopt获取一个套接字选项

socket函数——创建套接字

#include <sys/types.h>       
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

参数domain

函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。

名称含义名称含义
PF_UNIX、PF_UNIX本地通信PF_X25ITU-T X25 / ISO-8208协议
AF_INET、PF_INETIPv4 Internet协议PF_AX25Amateur radio AX.25
PF_INET6IPv6 Internet协议PF_ATMPVC原始ATM PVC访问
PF_IPXIPX-Novell协议PF_APPLETALKAppletalk
PF_NETLINK内核用户界面设备PF_PACKET底层包访问

参数type
type 函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。

名称含义
SOCK_STREAMTcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输
SOCK_DGRAM支持UDP连接(无连接状态的消息)
SOCK_SEQPACKET序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出
SOCK_RAWRAW类型,提供原始网络协议访问
SOCK_RDM提供可靠的数据报文,不过可能数据会有乱序

并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。

参数protocol
protocol用于制定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。

errno
函数socket()并不总是执行成功,有可能会出现错误,错误的产生有多种原因,可以通过errno获得:

含义
EACCES没有权限建立制定的domain的type的socket
EAFNOSUPPORT不支持所给的地址类型
EINVAL不支持此协议或者协议不可用
EMFILE进程文件表溢出
ENFILE已经达到系统允许打开的文件数量,打开文件过多
ENOBUFS/ENOMEM内存不足。socket只有到资源足够或者有进程释放内存
EPROTONOSUPPORT制定的协议type在domain中不存在

比如我们建立一个流式套接字可以这样:

int sock = socket(AF_INET, SOCK_STREAM, 0);

bind

#include <sys/types.h>         
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

在套接口中,一个套接字只是用户程序与内核交互信息的枢纽,它自身没有太多的信息,也没有网络协议地址和 端口号等信息,在进行网络通信的时候,必须把一个套接字与一个地址相关联,这个过程就是地址绑定的过程。许多时候内核会我们自动绑定一个地址,然而有时用 户可能需要自己来完成这个绑定的过程,以满足实际应用的需要,最典型的情况是一个服务器进程需要绑定一个众所周知的地址或端口以等待客户来连接。这个事由 bind的函数完成。

sockfd

就是我们调用socket函数后创建的socket 句柄或者称文件描述符号。

addr

addr是指向一个结构为sockaddr参数的指针,sockaddr中包含了地址、端口和IP地址的信息。在进行地址绑定的时候,需要弦将地址结构中的IP地址、端口、类型等结构struct sockaddr中的域进行设置之后才能进行绑定,这样进行绑定后才能将套接字文件描述符与地址等接合在一起。

由于历史原因,我们前后有两个地址结构: struct sockaddr 该结构定义如下:

struct sockaddr { 
    uint8_t sa_len;   
    unsigned short sa_family; /* 地址家族, AF_xxx */    
    char sa_data[14]; /14字节协议地址/ 
};

其实这个结构逐渐被舍弃,但是也还是因为历史原因,在很多的函数,比如connect、bind等还是用这个作为声明,实际上现在用的是第二个结构,我们需要把第二个结构强转成sockaddr。 struct sockaddr_in 其定义如下:

struct sockaddr_in { 
    uint8_t sa_len;   /* 结构体长度*/ 
    short int sin_family; /* 通信类型 */ 
    unsigned short int sin_port; /* 端口 */ 
    unsigned char sin_zero[8]; /* 未使用的*/ 
};

struct in_addr {   //sin_addr的结构体类型in_addr 原型
    unsigned long s_addr;     /存4字节的 IP 地址(使用网络字节顺序)。/
 };

在使用的时候我们必须指定通信类型,也必须把端口号和地址转换成网络序的字节序

addrlen
addr结构的长度,可以设置成sizeof(struct sockaddr)。使用sizeof(struct sockaddr)来设置套接字的类型和其对已ing的结构。

bind()函数的返回值为0时表示绑定成功,-1表示绑定失败,errno的错误值如表1所示。

含义备注
EADDRINUSE给定地址已经使用
EBADFsockfd已经绑定到其他地址
ENOTSOCKsockfd是一个文件描述符,不是socket描述符
EACCES地址被保护,用户的权限不足
EADDRNOTAVAIL接口不存在或者绑定地址不是本地UNIX协议族,AF_UNIX
EFAULTmy_addr指针超出用户空间UNIX协议族,AF_UNIX
EINVAL地址长度错误,或者socket不是AF_UNIX族UNIX协议族,AF_UNIX
ELOOP解析my_addr时符号链接过多UNIX协议族,AF_UNIX
ENAMETOOLONGmy_addr过长UNIX协议族,AF_UNIX
ENOENT文件不存在UNIX协议族,AF_UNIX
ENOMEN内存内核不足UNIX协议族,AF_UNIX
ENOTDIR不是目录UNIX协议族,AF_UNIX

比如这样:

struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(sfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) 
{
    perror("bind");
    exit(1);
}

listen

#include <sys/types.h>      
#include <sys/socket.h>
int listen(int sockfd, int backlog);

listen()函数将sockfd标记为被动打开的套接字,并作为accept的参数用来接收到达的连接请求。

sockfd

是一个套接字类型的文件描述符,具体类型为SOCK_STREAM或者SOCK_SEQPACKET。

backlog参数

用来描述sockfd的等待连接队列能够达到的最大值。当一个请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,或者如果底层协议支持重传(比如tcp协议),本次请求会被丢弃不作处理,在下次重试时期望能连接成功(下次重传的时候队列可能已经腾出空间)。
说起这个backlog就有一点儿历史了,等下文描述。

errno

含义
EADDRINUSE另一个套接字已经绑定在相同的端口上。
EBADF参数sockfd不是有效的文件描述符
ENOTSOCK参数sockfd不是套接字。
EOPNOTSUPP参数sockfd不是支持listen操作的套接字类型。

connect

#include <sys/types.h>         
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数说明如下

sockfd

系统调用 socket() 返回的套接字文件描述符。

serv_addr

保存着目的地端口和 IP 地址的数据结构 struct sockaddr_in。

addrlen

设置 为 sizeof(struct sockaddr_in)

errno

connect函数在调用失败的时候返回值-1,并会设置全局错误变量 errno。

含义
EBADF参数sockfd 非合法socket处理代码
EFAULT参数serv_addr指针指向无法存取的内存空间
ENOTSOCK参数sockfd为一文件描述词,非socket。
EISCONN参数sockfd的socket已是连线状态
ECONNREFUSED连线要求被server端拒绝。
ETIMEDOUT企图连线的操作超过限定时间仍未有响应。
ENETUNREACH无法传送数据包至指定的主机。
EAFNOSUPPORTsockaddr结构的sa_family不正确。

accept

#include <sys/types.h>        
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数说明

sockfd是由socket函数返回的套接字描述符,

参数addr和addrlen用来返回已连接的对端进程(客户端)的协议地址。如果我们对客户端的协议地址不感兴趣,可以把arrd和addrlen均置为空指针。

返回值

成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应地设定全局变量errno。

含义
EBADF非法的socket
EFAULT参数serv_addr指针指向无法存取的内存空间
ENOTSOCK参数sockfd为一文件描述词,非socket。
EOPNOTSUPP指定的socket并非SOCK_STREAM
EPERM防火墙拒绝此连线
ENOBUFS系统的缓冲内存不足
ENOMEM核心内存不足

特别需要说明下的是,这个accept是一个阻塞式的函数,对于一个阻塞的套套接字,一直阻塞,或者返回一个错误值,对于非阻塞套接字。accept有可能返回-1,但是如果errno的值为,EAGAIN或者EWOULDBLOCK,此时需要重新调用一次accept函数。

send和recv函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd :套接字
  • buf : 待发送或者接收的缓存
  • len : 如果是recv指期望接收的长度,如果是send指要发送的长度。
  • flags : 标志位,取值如下表:
flags说明recvsend
MSG_DONTROUTE绕过路由表查找
MSG_DONTWAIT仅本操作非阻塞
MSG_OOB发送或接收带外数据
MSG_PEEK窥看外来消息
MSG_WAITALL等待所有数据


errno

含义
EBADFsock不是有效的描述词
EFAULT内存空间访问出错
ENOTSOCKsock索引的不是套接字 当返回值是0时,为正常关闭连接;
EAGAIN套接字已标记为非阻塞,而接收操作被阻塞或者接收超时
ECONNREFUSED连线要求被server端拒绝。
EINTR操作被信号中断
EINVAL参数无效
ENOTCONN与面向连接关联的套接字尚未被连接上
ENOMEM内存不足

当返回值为-1时是不是一定就错误了,当返回值为0时该怎么做呢?

IP相关的数据结构和函数

数据结构

表示IP地址相关数据结构都定义在 netinet/in.h这里。
struct sockaddr注意这个IP地址是不区分IPv4和IPv6的,这个结构体是linux的网络编程接口中用来表示IP地址的标准结构体,bind、connect等函数
中都需要这个结构体,这个结构体是兼容IPV4和IPV6的。在实际编程中这个结构体会被一个struct sockaddr_in或者一个struct sockaddr_in6强制类型转换而来。

struct sockaddr { 
    uint8_t sa_len;   
    unsigned short sa_family; /* 地址家族, AF_xxx */    
    char sa_data[14]; /14字节协议地址/ 
};
typedef uint32_t in_addr_t;        网络内部用来表示IP地址的类型
struct in_addr
{
   in_addr_t s_addr;
};
struct sockaddr_in { 
    uint8_t sa_len;   /* 结构体长度*/ 
    short int sin_family; /* 通信类型 */ 
    unsigned short int sin_port; /* 端口 */ 
    unsigned char sin_zero[8]; /* 未使用的*/ 
};

我们电脑是有大小端的,有的电脑是大端,有的电脑是小端,在网络中统一规定使用大端,所以就把大端成为网络字节序。

相关函数

下面的函数用来实现IP地址的点分十进制和十六进制之间的转化。

是Linux中的库函数,用man 3查询。
inet_addr、inet_ntoa、inet_aton
inet_pton、inet_ntop

第一行的只能是和IPV4,第二行可以兼容IPV4和IPV6。所以最好使用第二行的函数。这几个函数内部都可以实现转为网络字节序,不必我们来担心了。

#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr)  //点分十进制ipv4地址转换为网络ipv4地址,失败返回INADDR_NONE
int inet_aton(const char* cp, struct in_addr* inp) //点分十进制ipv4地址转换为网络ipv4地址,成功为1,失败为0
char* inet_aton(struct addr_in in);      //网络ip4地址转为点分十进制ipv4地址,用字符串表示
#include <arpa/inet.h>
int inet_ptoa(int af, const char* src, void* dst) //点分转为网络
const char* inet_atop(int af, const void* src, char* dst, socklen_t cnt)

af: 协议族:AF_INET或AF_INET6

因为网络字节序是大端模式,

uint16_t htons(uint16_t hostshort) //主机序到网络序
uint32_t htonl(uint32_t hostlong)
uint16_t ntohs(uint16_t netshort)// 网络序到主机序
uint32_t ntohl(uint32_t netlong)

我们设置IP地址的时候要使用上面的前两个函数函数。
具体的使用方法,在后面的程序中可以看出来。

socket的一个例子,总结上述的问题

概念:端口号,实质就是一个数字编号,用来在我们一台主机中(主机的操作系统中)唯一的标识一个能上网的进程。端口号和IP地址一起会被打包到当前进程发出或者接收到的每一个数据包中。每一个数据包将来在网络上传递的时候,内部都包含了发送方和接收方的信息(就是IP地址和端口号),所以IP地址和端口号这两个往往是打包在一起不分家的。
端口号中前1024个数字已经被占用了,不可以随便使用,1024~65535之间的一般来说可以随便用

server.c服务器程序

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERPORT        9003
#define SERADDR        "192.168.1.141"        // ifconfig看到的服务器IP
#define BACKLOG        100


char recvbuf[100];


int main(void)
{
    // 第1步:先socket打开文件描述符
    int sockfd = -1, ret = -1, clifd = -1;
    socklen_t len = 0;
    struct sockaddr_in seraddr = {0};
    struct sockaddr_in cliaddr = {0};
    
    char ipbuf[30] = {0};
    
    
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    printf("socketfd = %d.\n", sockfd);
    
    // 第2步:bind绑定sockefd和当前电脑的ip地址&端口号
    seraddr.sin_family = AF_INET;        // 设置地址族为IPv4
    seraddr.sin_port = htons(SERPORT);    // 设置地址的端口号信息
    seraddr.sin_addr.s_addr = inet_addr(SERADDR);    // 设置IP地址
    ret = bind(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
    if (ret < 0)
    {
        perror("bind");
        return -1;
    }
    printf("bind success.\n");
    
    // 第三步:listen监听端口
    ret = listen(sockfd, BACKLOG);        // 阻塞等待客户端来连接服务器
    if (ret < 0)
    {
        perror("listen");
        return -1;
    }
    
    // 第四步:accept阻塞等待客户端接入
    clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
    printf("连接已经建立,client fd = %d.\n", clifd);
    
    // 服务器给客户端发
    strcpy(recvbuf, "hello world.");
    ret = send(clifd, recvbuf, strlen(recvbuf), 0);
    printf("发送了%d个字符\n", ret);
    
    return 0;
}

客户端程序

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>

#define SERADDR        "192.168.1.141"        // 服务器开放给我们的IP地址和端口号
#define SERPORT        9003

char sendbuf[100];

int main(void)
{
    // 第1步:先socket打开文件描述符
    int sockfd = -1, ret = -1;
    struct sockaddr_in seraddr = {0};
    struct sockaddr_in cliaddr = {0};
    
    // 第1步:socket
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("socket");
        return -1;
    }
    printf("socketfd = %d.\n", sockfd);
    
    // 第2步:connect链接服务器
    seraddr.sin_family = AF_INET;        // 设置地址族为IPv4
    seraddr.sin_port = htons(SERPORT);    // 设置地址的端口号信息
    seraddr.sin_addr.s_addr = inet_addr(SERADDR);    // 设置IP地址
    ret = connect(sockfd, (const struct sockaddr *)&seraddr, sizeof(seraddr));
    if (ret < 0)
    {
        perror("listen");
        return -1;
    }
    printf("成功建立连接\n");

    ret = recv(sockfd, sendbuf, sizeof(sendbuf), 0);
    printf("成功接收了%d个字节\n", ret);
    printf("client发送过来的内容是:%s\n", sendbuf);
    
    return 0;
}
Last modification:July 7th, 2019 at 11:35 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment