目录
理解端口号
理解源ip地址与目的IP地址
我们知道ip地址,在网络中可以用来标识主机的唯一性,也就是说我知道你的ip地址,我也就能向你发送数据,但我发送给你的数据,最终是一定要交付给进程的,但交给你哪个进程呢?我应该知道吗?应该,所以就有了端口号,端口号是用来标识主机中的唯一进程的,所以我们在网络中传输数据就只需要拿着目的ip地址和对应进程端口号即可把数据发送到指定进程,所以网络通信又被称为进程间通信,只不过这里是通过网络进行不同主机间通信,不是同一台主机的进程间进行通信。
认识端口号
端口号(port)是传输层协议的内容。
- 端口号是一个 2 字节 16 位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP 地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用。
端口号也是有着范围划分的:
- 0 - 1023: 知名端口号, HTTP, FTP, SSH 等这些广为使用的应用层协议, 他们的
端口号都是固定的. - 1024 - 65535: 操作系统动态分配的端口号. 客户端程序的端口号, 就是由操作
系统从这个范围分配的
理解端口号与pid关系
我们的进程不是都有进程pid吗,既然有了可以唯一标识一个进程的标志,那为什么我们还要再引入端口号的概念呢???其实是因为进程ID是内核级的,而端口号属于用户级。
- 进程pid是用来标识正在运行中的进程,进程退出就找不到了。
- 进程pid每次运行都是变化的,不能保证每次运行的pid都是一样的。
- 进程 ID 属于系统概念, 技术上也具有唯一性, 确实可以用来标识唯一的一个进程, 但是这样做, 会让系统进程管理和网络强耦合, 实际设计的时候, 并没有选择这样做。
理解socket编程
由于ip+port能够唯一标识某台主机上的唯一一个进程,所以我们把这种ip + port起个名字叫套接字socket,我们只需要源主机的ip+port,目的主机的ip+port就能进行进程间通信了!
理解网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之分,那么怎么定义网络中的数据流的地址呢?在网络中我们是采用的大端字节序存储的,假如A主机向B主机发送数据,A主机是小端存储,B主机是大端存储,A主机发送的数据到达B主机,如果我们不对传输的数据进行统一处理,B主机接收到的数据就可能会是乱码,为了避免这种情况,我们规定进入网络的数据都要转为网络字节序,而TCP/IP协议规定网络字节序是采用的大端存储,低字节放在高地址处。
为使网络程序具有可移植性,使同样的 C 代码在大端和小端计算机上编译后都能正常运
行,可以调用以下库函数做网络字节序和主机字节序的转换。
这些函数名很好记,h 表示 host,n 表示 network,l 表示 32 位长整数,s 表示 16 位短整数。
- htonl 表示将 32 位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
- htons表示将 16 位的短整数从主机字节序转换为网络字节序,例如将端口号地址转换后准备发送。
- ntohl 表示将 32 位的长整数从网络字节序转换为主机字节序,例如将接收的IP地址转换后准备发送。
- ntohs表示将 16 位的短整数从网络字节序转换为主机字节序,例如将接收的端口号地址转换后准备发送。
socket编程接口
常见的API
cpp
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听 socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
创建socket套接字
使用socket创建一个套接字进行通信
- domain:地址簇,常见的右AF_INET(ipv4)和AF_INET6(ipv6)
- type:套接字类型,常见的有SOCK_STREAM(TCP)和SOCK_DGRAM(UDP).
- protocol:协议,通常设置为0,也可指定协议。
- 返回值:成功返回一个套接字文件描述符,失败返回-1,error被设置。
bind绑定套接字
bind函数可以绑定一个ip地址和端口号
- sockfd:套接字文件描述符
- addr:指向一个sockaddr结构体的结构体指针,该结构体包含了ip地址与端口号等信息,对于IPv4定义在sockaddr_in结构体中,IPv6则在sockaddr_in中定义。
- addrlen:该结构体对象的大小,通常用sizeof获取。
- 返回值:成功返回0,错误返回-1,error被设置。
listen开始监听
listen函数用在tcp中用来监听客户端的连接,类似一个老板要不断的监管自己的公司。
- sockfd:需要监听的套接字文件描述符
- backlog:要求连接的最大数量
accept接收请求
accept是一个系统调用,用于在服务器端接受来自客户端的连接请求。
- sockfd:套接字文件描述符,用来监听客户端连接请求的套接字,在tcp中可以理解为用来接收外部连接的套接字文件描述符,该函数会返回一个提供IO服务新的套接字文件描述符。
- addr:输入输出型参数,获取发送方的IP地址端口号等各种信息。
- addrlen:发放方sockaddr结构体的大小,调用时设置为结构体的总大小,返回时返回实际使用的大小。
- 返回值是一个新的文件描述符,可以提供IO服务,这个新的这个文件描述符服务器端就可以与客户端进行通信。
connect建立连接
connect用于客户端程序,将其套接字连接到服务器端的套接字,以便进行通信。
- sockfd:表示要建立连接的套接字描述符。
- addr:表示指向目标服务器端套接字的地址结构体的指针。在IPv4中,该结构体类型为struct sockaddr_in,而在IPv6中,该结构体类型为struct sockaddr_in6。
- addrlen:表示目标服务器端地址结构体的长度。
- 返回时connect连接成功后会进行bind,返回成功,返回0,失败为-1,错误码被设置
recvfrom接收数据
recvfrom()函数的一个常见用法是在UDP套接字上接收数据报。在这种情况下,可以使用它来接收来自任意源地址的UDP数据报,并获取发送方的地址,以便进行后续处理。
- sockfd:指定要接收数据的套接字
- buf:指定接收数据的缓冲区
- len:指定要接收数据缓冲区的最大长度
- flags:指定接收数据的方式(一般设置为0)
- src_addr:输入输出型参数,获取发送方的IP地址端口号等各种信息。
- addrlen:发放方sockaddr结构体的大小,调用时设置为结构体的总大小,返回时返回实际使用的大小。
sendto发送数据
sendto函数将数据报发送到指定的目的地址。
- sockfd:指定要发送数据的套接字文件描述符。
- buf:指定发送数据的缓冲区。
- len:指定要发送数据的长度。
- flags:指定发送数据的方式(一般设置为0)
- dest_addr:输入输出型参数,指向目的地址的的结构体指针,可以是IP地址和端口号。
- addrlen:目的地址sockaddr结构体的大小。
- 成功则返回发送的字节数,失败返回-1,error被设置。
sockaddr结构
由于各种网络协议的格式并不相同,所以就有了sockaddr结构来,在使用sockaddr结构时底层上派生了其他相对应的具体结构,sockaddr只是作为基类来使用,例如就派生的结构体有:struct sockaddr_in,struct sockaddr_un。这其实是利用了多态的思想实现的!!!
- IPv4 和 IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,包括 16 位地址类型, 16 位端口号和 32 位 IP 地址.
- IPv4、 IPv6 地址类型分别定义为常数 AF_INET、 AF_INET6. 这样,只要取得某种 sockaddr 结构体的首地址,不需要知道具体是哪种类型的 sockaddr 结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API 可以都用 struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收 IPv4, IPv6, 以及 UNIX Domain Socket 各种类型的 sockaddr 结构体指针做为参数。
sockaddr底层结构
cpp
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
sockaddr_in底层结构
cpp
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};
in_addr结构
cpp
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr 用来表示一个 IPv4 的 IP 地址. 其实就是一个 32 位的整数。
网络命令
Ping命令
ping www.baidu.com可以查看自己的网络是否正常运行
ping -c 5 www.baidu.com可以查看5条后停止
netstat
netstat用来查看网络状态
- n 拒绝显示别名, 能显示数字的全部转化成数字
- l 仅列出有在 Listen (监听) 的服務状态
- p 显示建立相关链接的程序名
- t (tcp)仅显示 tcp 相关选项
- u (udp)仅显示 udp 相关选项
- a (all)显示所有选项, 默认不显示 LISTEN 相关
可以查看自己启动的网络服务状态
普通用户默认是看不到别的pid等信息的,只有超级用户全部都能看到。
watch
watch -n 1 netstat -uap 每隔一秒执行一次netstat -uap命令
pidof
pidof用来查看服务器的进程id时非常方便
pidof + 进程名
pidof UdpServerMain | xargs kill -9 xargs让我们从标准输入中读转为在从命令行参数中去读
地址转换函数
inet_ntoa
inet_ntoa可以把四字节地址转换为字符串类型。
但是在多线程使用inet_ntoa时可能不是线程安全的(因为这个函数转换时会自己维护一个类似缓冲区的地方,类似文件fopen的返回值),一般我们不会使用它,而使用inet_ntop。
inet_ntop
inet_ntop可以把四字节地址转换为字符串类型。
但是这个缓冲区可以由我们自己来维护了,更加方便。
- af:网络协议,一般传AF_INET
- src:传入四字节地址
- dst:目的缓冲区
- size:缓冲区大小
inet_pton
inet_pton是把字符串类型转换为四字节ip地址
- af:网络协议,一般传AF_INET
- src:传入缓冲区地址
- dst:转换的目的位置
这里的p:process, n: net