手把手写C++服务器(21):Linux socket网络编程入门基础
Posted 沉迷单车的追风少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器(21):Linux socket网络编程入门基础相关的知识,希望对你有一定的参考价值。
本系列文章导航: 手把手写C++服务器(0):专栏文章-汇总导航【更新中】
前言:刚开始写C++服务器的时候,我们进行网络编程肯定是使用socket API,等熟练之后,会根据我们自己的需要,封装这些API组成自己的网络编程库。如何优雅地封装?这是一个哲学问题,非常能体现C++程序员的功底。但是首先要熟悉socket的常见用法,这一篇博客带你入门,并用手把手写C++服务器(18):TCP紧急传输的方法——带外数据 (原理与代码示例)这篇文章作为综合应用实例。
目录
通用socket地址:sockaddr和sockaddr_storage
TCP/IP专用socket地址:sockaddr_in和sockaddr_in6
IP地址转换函数:inet_addr、 inet_aton、inet_ntoa
什么是socket?
socket(套接字)用来描述IP地址和端口,是通信链的句柄,应用程序可以通过 Socke向网络发送请求或者应答网络请求。socket是支持TCP/P协议的网络通信的基本操作单元是对网络通信过程中端点的抽象表示,包含了进行网络通信所必须的五种信息连接所使用的协议;本地主机的P地址;本地远程的协议端口;远地主机的IP地址以及远地进程的协议端口。
socket建立连接的过程
常用socket函数一览
不清楚的地方记得查询linux socket手册!
https://man7.org/linux/man-pages/man2/socket.2.html
通用socket地址:sockaddr和sockaddr_storage
sockaddr定义如下:
#include <bits/socket.h>
struct sockaddr {
sa_family_t sa_family; //地址族类型
char sa_data[14]; //socket地址值
};
由于sockaddr里面只能存放14个字节,明显不够用!因此sockaddtr_stroage就用了作用:
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align;
char__ss_padding[128-sizeof(__ss_align)];
}
Unix专用socket地址:sockaddr_un
#include <sys/un.h>
strcut sockaddr_un {
sa_family_t sin_family; //地址族
char sun_path[108]; // 文件路径名
};
TCP/IP专用socket地址:sockaddr_in和sockaddr_in6
sockaddr_in对应IPv4,sockaddr_in6对应IPv6
struct sockaddr_in
{
sa_family_t sin_family;/*地址族: AF_INET*/
u_int16_t sin_port;/*端口号, 要用网络字节序表示*/
struct in_addr sin_addr;/*IPv4地址结构体, 见下面*/
};
struct sockaddr_in6
{
sa_family_t sin6_family;/*地址族: AF_INET6*/
u_int16_t sin6_port;/*端口号, 要用网络字节序表示*/
u_int32_t sin6_flowinfo;/*流信息, 应设置为0*/
struct in6_addr sin6_addr;/*IPv6地址结构体, 见下面*/
u_int32_t sin6_scope_id;/*scope ID*/
};
注意:实际使用的过程中,所有专用socket地址类型都需要强制转换成sockaddr,统一类型。
IP地址转换函数:inet_addr、 inet_aton、inet_ntoa
inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址:
#include<arpa/inet.h>
in_addr_t inet_addr(const char*strptr);
inet_aton函数完成和inet_addr同样的功能, 但是将转化结果存储于参数inp指向的地址结构中。 它成功时返回1, 失败则返回0。
#include<arpa/inet.h>
int inet_aton(const char*cp,struct in_addr*inp);
inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
#include<arpa/inet.h>
char*inet_ntoa(struct in_addr in);
创建socket:socket()
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);
参数释义:
1、domain:告诉系统使用哪个底层协议族。 对TCP/IP协议族而言, 该参数应该设置为PF_INET(Protocol Family of Internet, 用于IPv4) 或PF_INET6(用于IPv6) ; 对于UNIX本地域协议族而言, 该参数应该设置为PF_UNIX。
2、type:指定服务类型。 服务类型主要有SOCK_STREAM服务(流服务) 和SOCK_UGRAM(数据报) 服务。 对TCP/IP协议族而言, 值取SOCK_STREAM表示传输层使用TCP协议, 取SOCK_DGRAM表示传输层使用UDP协议。
3、protocol:是在前两个参数构成的协议集合下, 再选择一个具体的协议。 不过这个值通常都是唯一的(前两个参数已经完全决定了它的值) 。 一般设置为0, 表示使用默认协议。
函数返回:
调用成功时返回一个socket文件描述符, 失败则返回-1并设置errno。
绑定socket地址:bind()
只有服务端需要绑定地址,客户端使用系统自动分配的匿名地址。
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr*my_addr,socklen_t addrlen);
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符, addrlen参数指出该socket地址的长度。
函数返回:
bind成功时返回0, 失败则返回-1并设置errno。 其中两种常见的errno是EACCES和EADDRINUSE。
- EACCES, 被绑定的地址是受保护的地址, 仅超级用户能够访问。 比如普通用户将socket绑定到知名服务端口(端口号为0~1023) 上时, bind将返回EACCES错误。
- EADDRINUSE, 被绑定的地址正在使用中。 比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
监听socket地址:listen()
创建一个监听队列以存放待处理的客户连接:
#include<sys/socket.h>
int listen(int sockfd,int backlog);
sockfd参数指定被监听的socket。 backlog参数提示内核监听队列的最大长度。 监听队列的长度如果超过backlog, 服务器将不受理新的客户连接, 客户端也将收到ECONNREFUSED错误信息。listen成功时返回0, 失败则返回-1并设置errno。
接受socket连接:accept()
从listen监听队列中接受一个连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr*addr,socklen_t*addrlen);
sockfd参数是执行过listen系统调用的监听socket。 addr参数用来获取被接受连接的远端socket地址, 该socket地址的长度由addrlen参数指出。 accept成功时返回一个新的连接socket, 该socket唯一地标识了被接受的这个连接, 服务器可通过读写该socket来与被接受连接对应的客户端通信。 accept失败时返回-1并设置errno。
注意:accept只是从监听队列中取出连接, 而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态) , 更不关心任何网络状况的变化。
发起socket连接:connect()
客户端使用connect()与服务端建立连接:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd,const struct sockaddr*serv_addr,socklen_t addrlen);
sockfd参数由socket系统调用返回一个socket。 serv_addr参数是服务器监听的socket地址, addrlen参数则指定这个地址的长度。
函数返回:
connect成功时返回0。 一旦成功建立连接, sockfd就唯一地标识了这个连接, 客户端就可以通过读写sockfd来与服务器通信。 connect失败则返回-1并设置errno。 其中两种常见的errno是ECONNREFUSED和ETIMEDOUT, 它们的含义如下:
- ECONNREFUSED, 目标端口不存在, 连接被拒绝。
- ETIMEDOUT, 连接超时。
关闭socket连接:close()
关闭普通文件描述符的系统调用:
#include<unistd.h>
int close(int fd);
fd参数是待关闭的socket。 不过,close系统调用并非总是立即关闭一个连接, 而是将fd的引用计数减1。只有当fd的引用计数为0时, 才真正关闭连接。 多进程程序中, 一次fork系统调用默认将使父进程中打开的socket的引用计数加1, 因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
立即终止socket连接:shutdown()
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
howto参数决定了shutdown的行为,具体参看手册。
TCP数据读写:recv()、send()
TCP是流协议,recv()和send()用于读写缓冲区:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd,void*buf,size_t len,int flags);
ssize_t send(int sockfd,const void*buf,size_t len,int flags);
recv读取sockfd上的数据, buf和len参数分别指定读缓冲区的位置和大小, flags参数通常设置为0即可。
recv成功时返回实际读取到的数据的长度, 它可能小于我们期望的长度len。 因此我们可能要多次调用recv, 才能读取到完整的数据。
send往sockfd上写入数据, buf和len参数分别指定写缓冲区的位置和大小。 send成功时返回实际写入的数据的长度, 失败则返回-1并设置errno。
综合应用实例:带外数据
https://xduwq.blog.csdn.net/article/details/119182975
参考:
- https://www.runoob.com/w3cnote/android-tutorial-socket1.html
- 《linux高性能服务器编程》
- https://www.runoob.com/w3cnote/android-tutorial-socket-intro.html
- 《Linux多线程服务端编程》
- 《C++并发编程实战》
以上是关于手把手写C++服务器(21):Linux socket网络编程入门基础的主要内容,如果未能解决你的问题,请参考以下文章
手把手写C++服务器(22):Linux socket网络编程进阶第一弹