【Linux网络】网络套接字编程
套接字编程
- 一,理解端口号
- 二,初识TCP/UDP协议
- 三,网络字节序
- 四,UDP套接字编程常用API
- 4.1 struct sockaddr类型
- 4.2 socket接口
- 4.3 bind接口
- 4.4 recvfrom
- 4.5 sendto
- 五,TCP套接字常用API
- 5.1 listen接口
- 5.2 accept接口
- 5.3 connect接口
- 六,补充
- 6.1 心跳机制
- 6.2 守护进程
- 6.3 tcp底层发送细节
- 七,总结
一,理解端口号
我们网络通信中,实际上是两台主机上的进程之间的通信
对通信双方而言:
- 要先能够把数据交给对方的机器上 (通过 ip 地址来找)
- 找到指定的进程 (通过 port:端口号 来找到)
端口号用来标识网络通信中指定机器中的一个进程的唯一性
所以(ip,port)用来标识互联网中进程的唯一性
ip + port 就是套接字socket
网络进程要和port进行绑定,但是进程pid也可以标识一个进程,为什么还要port来标识唯一性?
这是因为:
- 其他模块(进程管理) 要和 网络 进行解耦
- port是专门用来网络通信的
二,初识TCP/UDP协议
为了理解下面套接字编程,我们先来简单了解一下传输层的两个协议
tcp协议有下面几个特点:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
udp有下面几个特点:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
tcp是可靠传输,需要先建立连接,面向字节流等,udp是不可靠传输,适用于对数据可靠性不高的场景,比如视频传输,直播等等。
可靠不可靠体现在面对异常情况时,tcp会进行可靠性处理,比如:丢包重传,乱序后排序等。而udp不做处理,tcp保证了可靠性,所以它更加复杂,而udp相比更简单。
三,网络字节序
这里我们再看一下网络字节序,先来看这样一个例子
两个主机要进行网络通信,那么就要进行收发数据,而不同的机器有不同的存储策略,大端或者小端
而且还要知道:
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
所以到达网络的数据,都必须是大端,这样所有主机收到数据时也都知道是大端
最后在使用时,转换为自己对应的存储模式即可
系统也会我们提供了网络字节序和主机序列的转换系统调用
很好理解,h表示host,也就是主机,n表示net网络,后面的s和l表示short或者long
四,UDP套接字编程常用API
常用的API有如下:
4.1 struct sockaddr类型
这里的接口都有一个 struct sockaddr
的参数。这里我们说一下这个结构。
首先,socket有很多种类:
- unix socket,域间 socket ,用于本主机之间的通信,和命名管道类似
- 网络socket, 采用 ip + port
- 原始socket, 类似于在应用层绕过udp,tcp来进行网络通信,一般用来编写网络工具,不以传输数据为目的
这些不同的socket用的都是一套接口
struct sockaddr是一个通用的地址类型,struct sockaddr_in 和 struct sockaddr_un 传入套接字相关接口时,将他们强转为 struct sockaddr 类型,底层取出它们的前两个字节,也就是16位地址类型,判断这个具体是什么类型
所以这就是C风格的多态,struct sockaddr作为基类,让不同的套接字去基础,以达到实现不同的套接字可以用一套接口进行通信
4.2 socket接口
创建套接字,本质也是创建一个文件,所以对网络的操作就像对文件的操作
int socket(int domain, int type, int protocol);
- 返回值是个
文件描述符
,Linux下一切皆文件,打开网卡也就相当于文件操作 - 第一个参数表示设置的通信方式,域间套接还是网络通信,表示哪个域:比如 AF_INET 表示创建一个网络通信的文件
- 第二个参数表示套接字类型: SOCK_STREAM 表示流式套接,用于tcp,SOCK_DGRAM 用于udp
- 第三个表示用tcp还是udp,但是前两个固定好后,第三个缺省为0就可以了
使用时就可以:
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
4.3 bind接口
创建完后,就要进行绑定(如果是客户端,则不用显式绑定,一般由OS去随机绑定端口)
绑定套接字,指定网络信息
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 第一个参数表示要绑定的套接字,也就是刚刚 socket 所创建的网络套接字
- 第二个表示使用的套接字结构体,我们实际上使用的是 struct sockaddr_in
- 第三个表示结构体大小
- 成功返回0,失败返回-1
在使用前,我们需要设置套接字结构
// 绑定套接字
struct sockaddr_in local;
bzero(&local, sizeof(local));
// 填充服务端信息
local.sin_family = AF_INET;
local.sin_port = htons(_port); //主机序列转成网络序列
local.sin_addr.s_addr = inet_addr(_ip.c_str());
我们先定义出一个 sockaddr_in 结构,然后将其清空,接下来就去填充。
我们来看看 sockaddr_in 结构:
这里的inet_addr是将4字节IP转换为网络序列
接下来就要把设置好的套接字设置进内核中
// 将填充的内容设置进内核中
int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n != 0)
{lg.LogMessage(Fatal, "bind err,%d :%s\n", errno, strerror(errno));exit(Bind_Err);
}
注意:这里要将local强转为struct sockaddr结构
4.4 recvfrom
udp中绑定完后就可以直接收发消息了
udp中收消息用recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
- 第一个参数表示收哪个套接字的消息
- 第二个表示收到数据放在的缓冲区,用户级缓冲区
- 第三个表示用户级缓冲区大小,也表示期望收到的字节大小
- 第四个表示收消息的方式,我们设置为0阻塞式收消息
- 第五个和第六个参数是一个输出型参数,可以存储客户端的信息,可以得到client的ip和port
- 返回值是实际收到的字节大小,失败返回-1
4.5 sendto
udp发消息常用sendto
ssize_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
- 前四个参数和收消息一样,第一个表示通过哪个套接字发
- 第五个和第六个表示收消息中 获取到发消息的 client的IP和port 的结构体
- 返回值表示实际发过去的字节数,失败返回-1
- 这里有我写的几个udp套接字的例子,大家可以去我的gitee仓库看看:
- UDPecho
- 远程执行命令
- udp简单聊天室
五,TCP套接字常用API
tcp和udp使用起来大同小异,只是多了些步骤。
所以在前面几个步骤和udp没有任何差别,唯一的区别的就在创建套接字时传入的第二个参数改为 SOCK_STREAM
-
- 创建套接字
// 1.创建套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);
-
- 绑定套接字
// 2.填充本地服务器网络信息
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
// 3.绑定网络套接字
bind(_listensock, CONV(&local), sizeof(local))
5.1 listen接口
tcp协议是可靠连接,c/s双方要进行数据通信,必须要
先建立连接
但是一般都是先由客户端发起连接,tcp服务器就需要一直等待客户端建立连接
所以tcp第一个不同就是要设置监听
将打开的socket设置为监听状态,以便它可以接受来自客户端的连接请求
int listen(int sockfd, int backlog);
- 第一个参数是创建好的套接字
- 第二个参数是全连接队列长度,表示在队列中等待处理的最大连接数
- 返回值,成功返回0,失败返回-1
使用:
const static int default_backlog = 5;
// 4.设置socket为监听状态,tcp特有的操作
listen(_listensock, default_backlog)
5.2 accept接口
设置为监听状态后,如何知道有没有连接到来呢?
所以就要用accept
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 第一个参数是服务端的套接字描述符
- 第二,三个参数是输入输出型参数,和udp一样,会带出客户端的信息
- 返回的是一个新的文件描述符,表示返回一个新的sockfd,用来给客户端提供服务
tcp是面向连接的,服务器端需要
accept,这里会生成新的sockfd用来提供服务
,而socket创建套接字返回的sockfd用来获取连接
所以后面我们就通过accept返回的sockfd来进行发送或者接收数据。tcp是面向字节流的,所以tcp这里我们使用write
和read
来进行对套接字的读写。
使用:
int sockfd = accept(_listensock, CONV(&peer), &len);
5.3 connect接口
还有一个和udp不同的是,因为tcp是面向连接的,所以tcp客户端在创建好套接字后,不能和udp一样直接recvfrom和sendto,而是先发起连接。
初始化一个连接,在connect成功后自动绑定
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 这个sockfd是客户端创建的套接字描述符
- 第二个addr表示要向哪个服务点发起连接
- 成功返回0,失败返回-1
使用:
// 3.客户端向服务器发起connect
// 填充服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 1. 字符串ip->4字节IP 2. 网络序列
n = connect(sockfd, (struct sockaddr*)server, sizeof(server));
六,补充
6.1 心跳机制
心跳机制:为了检查我们的服务器在任意时刻是否都是健康的,我们就每隔一段时间向服务器发送最小请求,如果得到回复则说明是健康的
6.2 守护进程
守护进程
网络服务器实际上是在Linux后台运行的,而不是bash前台,是以后台守护进程(精灵进程)的方式执行的
基本概念:
-
进程组
默认是在一个会话
中的 -
同时启动多个进程时,可以同属于一个进程组,组id一般是多个进程中的第一个
-
每次登录Linux时,OS会给我们提供 一个bash和 一个终端(用来给用户提供命令解析), 这个
bash和终端
叫会话
-
命令行中所有的进程都默认在当前会话中的一个进程组中(一个进程也可以是一个进程组)
任何时候,一个会话内部可以存在很多个进程组,但是默认任意一个时刻,只允许一个进程组在前台
前台任务—和终端和键盘相关的,可以I/O的
给指令后加 &
变成后台
jobs
可以查看当前会话内的所有任务
fg 后台任务编号 ---可以再将这个任务变为前台
ctrl + z
暂停任务,并切换到后台,
bg
编号 :将后台暂停的任务唤醒
所写的服务器都要受当前的会话影响,如果退出当前会话,则服务器也会退出。如果让其不受影响,就守护进程化
守护进程是一个独立的会话,不隶属于任何一个bash
int setsid(void);
这个接口会将调用这个函数的进程变为守护进程
同时这个进程不能是进程组的组长,如果是自成进程组,一般通过创建子进程,再让父进程退出
守护进程一般是孤儿进程,其父进程是系统进程1号进程
(一般守护进程的命名上以 d 结尾)
启动后通过下面的指令查找守护进程:
ps ajx | head-1 && ps ajx | grep 进程名
6.3 tcp底层发送细节
tcp协议中,客户端和服务端在内核中都有一个发送缓冲区
和接收缓冲区
- write / send 的作用只是将用户要发送的内容拷贝到发送缓冲区中,当发送缓冲区满的时候,write/send就会
阻塞
- read / recv 的作用也只是将客户端发送到服务端的接收缓冲区的内容拷贝到用户自己的缓冲区中,接收缓冲区为空时,read/recv就会
阻塞
而什么时候真正发送,发送失败了怎么办都是由 内核–TCP
传输控制协议
决定的,所以TCP实际通信的时候是双方的OS之间的通信
这个也就是一种生产者消费者模型
发送方和接收方可以同时地把自己发送缓冲区的内容向对方的接收缓冲区拷贝(因为双方都有发送缓冲区和接收缓冲区),所以TCP是全双工的,udp全双工是类似的。
- 在上层用户层面,我们只调用send/recv就可以,但是在底层,发送了多少数据,接收方不一定会全部接受(因为接收方的接收缓冲区可能不够了),tcp是面向字节流的,只关心字节数,所以是按照字节数来发送的,不知道是否是一个完整的报文。所以就要需要我们自己来明确报文之间的边界(
粘包问题
),然后再进行序列反序列化
。 - 而udp要么不发送,发就发完整的报文,其在内核中已经明确了报文之间的边界(udp面向数据报)
七,总结
这一节介绍了网络套接字编程中接口的使用,我们可以用这些接口来编写一些简单的服务器程序,下面是我写的几个tcp服务器程序,大家可以来参考一下:
tcp_echo服务器
tcp网络计算机