文章目录
- 网络字节序
- socket编程
- socket 常见API
- sockaddr结构
- UDP编程
- 创建socket
- 绑定socket
- sendto发送数据
- recvform接收数据
- 关闭socket
- TCP编程
- 创建socket
- 绑定socket
- listen监听套接字
- accept服务端接收连接套接字
- connect客户端连接套接字
- send发送数据
- recv接收数据
- 关闭socket
- 工具
- netstat
- telnet
- 地址转换函数
- socket编程注意细节
- 代码案例
网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
-
h表示host,n表示network,l表示32位长整数,s表示16位短整数。
-
htonl表示将32位的长整数从主机字节序转换为网络字节序。
-
ntohl表示将32位的长整数从网络字节序转换为主机字节序。
socket编程
socket套接字通常指的是封装了ip和port的结构体,其是网络编程中的一种通信机制,支持TCP/IP的网络通信的基本操作单元,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
socket 常见API
// 创建 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);
sockaddr结构
套接字分类:
- 域间套接字 —— 本地通信
- 原始套接字 —— 允许绕过传输层,直接跟底层打交道,主要用来写一些工具。
- 网络套接字 —— 网络通信
理论上是三种应用场景,对应的应该是三套接口,但是Linux设计套接字的时候不想设计过多的接口,所以Linux将所有的接口进行了统一,只使用sockaddr结构体来描述这三种场景。 但是真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址。
UDP编程
创建socket
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
//参数
domain:指定网络层的协议
AF_INT: 使用ipv4版本的ip协议
AF_INT6:使用ipv6版本的ip协议
AF_UNIX:本地通信
type: 指定套接字的类型
SOCK_DGRAM :使用UDP数据报套接字
SOCK_STREAM:使用TCP字节流套接字
protocol:指定使用的协议
0:使用套接字类型对应的默认协议
绑定socket
int bind(int socket, const struct sockaddr* address,socklen_t address_len);*
//参数
sockfd: socket函数返回的套接字描述符
addr: 地址信息结构体成员,struct sockaddr 是一个通用地址信息结构
address_len:地址信息结构的长度
sendto发送数据
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
//参数
sockfd: 套接字描述符
buf: 要发送的数据
len: 发送数据的长度
flags: 0-阻塞发送
dest_addr: 目标主机的地址信息结构体
addrlen: 目标主机地址信息结构体的长度
//返回值
成功返回发送的字节数量,失败返回-1
recvform接收数据
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,struct sockaddr* src_addr, socklen_t* addrlen);
//参数
sockfd: 套接字描述符
buf: 将数据接收到buf当中
len: buf的最大接收能力
flags: 0-阻塞接收
src_addr: 数据来源的主机地址信息结构体
addrlen: 输入输出型参数
//返回值
成功则返回实际接收到的字符数,失败返回-1,错误原因会存于errno中
关闭socket
close(int sockfd);
//sockfd: 套接字描述符
TCP编程
创建socket
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
//参数
domain:指定网络层的协议
AF_INT: 使用ipv4版本的ip协议
AF_INT6:使用ipv6版本的ip协议
AF_UNIX:本地通信
type: 指定套接字的类型
SOCK_DGRAM :使用UDP数据报套接字
SOCK_STREAM:使用TCP字节流套接字
protocol:指定使用的协议
0:使用套接字类型对应的默认协议
//返回值:
socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,程序可以像读写文件一样用read/write在网络上收发数据;调用出错则返回-1
绑定socket
int bind(int socket, const struct sockaddr* address,socklen_t address_len);*
//参数
sockfd: socket函数返回的套接字描述符
addr: 地址信息结构体成员,struct sockaddr 是一个通用地址信息结构
address_len:地址信息结构的长度
listen监听套接字
//listen()声明sockfd处于监听状态
int listen(int sockfd, int backlog);
//参数
sockfd: 套接字描述符
backlog:已完成连接队列的大小
//返回值:
成功:0
失败:-1
当客户端和服务端进行三次握手的时候会存在两种状态:连接还未建立和连接已建立,此时操作系统内核中就会存在两个队列:未完成连接队列和已完成连接队列。当完成三次握手后会由未完成连接队列放到已完成连接队列,而backlog就是已完成连接队列的大小,backlog影响了服务端并发接收连接的能力。
accept服务端接收连接套接字
//从已经完成连接队列中获取已经完成三次握手的连接,没有连接时,调用accept会阻塞等待。
int accept(int sockfd, struct sockaddr* addr, socklen_t * addrlen);
//参数
sockfd:套接字描述符(listen_sockfd)
addr:输出型参数,保存客户端地址信息结构(客户端IP,客户端的端口)
addrlen:输入输出参数,传入缓冲区的大小,传出客户端地址信息结构的长度
//返回值
成功:返回新连接的套接字描述符
失败:返回-1
三次握手的时候是对listen_sockfd进行操作,当调用accept()会在Tcp服务端内部创建一个新的套接字new_sockfd,三次握手之后的数据收发都是多new_sockfd进行操作。
connect客户端连接套接字
//客户端需要调用connect()连接服务器
int connect(int sockfd, const struct sockaddr * addr,socklen_t addrlen);
//参数
sockfd:套接字描述符(listen_sockfd)
addr:服务端地址信息结构(服务端IP,服务端的端口)
addrlen:服务端地址信息结构的长度
//返回值
成功:返回0
小于0,连接失败
send发送数据
ssize_t send(int sockfd, const void * buf, size_t len, int flags);
//参数
sockfd:套接字描述符(new_sockfd)
buf:待要发送的数据
len:发送数据的长度
flags:
0:阻塞发送
MSG_OOB:发送带外数据,在紧急情况下所产生的数据,会越过前面进行排队的数据优先进行发送。
//返回值
大于0:返回发送的字节数量
-1:发送失败
recv接收数据
ssize_t recv(int sockfd, void * buf, size_t len, int flags);
//参数
sockfd:套接字描述符(new_sockfd)
buf:将接收的数据放到buf
len:buf的最大接收能力
flags:0:阻塞发送;如果客户端没有发送数据,调用recv会阻塞
//返回值
大于0:正常接收了多少字节数据
等于0:对端将连接关闭了
小于0:接受失败
关闭socket
close(int sockfd);
//sockfd: 套接字描述符
工具
netstat
netstat -anp | grep [端口号]
- 查看端口的使用情况
telnet
- 进入cmd,使用telnet模仿TCP三次握手建立连接,在cmd窗口输入 “tenlet + 公网IP + 端口号” 即可模拟测试
地址转换函数
sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址,但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换
字符串转in_addr的函数:
in_addr转字符串的函数:
inet_ntoa函数把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。
socket编程注意细节
- 客户端没有必要调用bind()固定一个端口号,否则如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。
- 服务器也不是必须调用bind(),但如果服务器不调用bind(), 内核会自动给服务器分配监听端口, 每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
- 多进程的客户端代码和单进程是一样的,父进程负责accept,子进程负责数据的接收和发送,若子进程一直不退出,则父进程一直在等待,永远无法接收新连接,可以使用自定义信号处理方式将SIGCHLD信号重新定义,当子进程退出发出SIGCHLD信号时,父进程则对子进程的资源进行回收。
- 建立好了tcp连接之后,我们就可以把得到的fd当作文件描述符来使用,也可以使用read和 write函数进行读写。
代码案例
UDP案例
UDP · 程序员Jared/Linux - 码云 - 开源中国 (gitee.com)
TCP案例
TCP · 程序员Jared/Linux - 码云 - 开源中国 (gitee.com)