一、TCP / UDP 通信协议对比
① TCP 面向连接(如打电话要先拨号建立连接);UDP 是无连接的,即发送数据之前不需要建立连接
② TCP 提供可靠的服务,也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
③ TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的,UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)
④ 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
⑤ TCP 首部开销20字节;UDP 的首部开销小,只有8个字节
⑥ TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道
二、字节序
1、概述
字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序
Little endian
小端字节序:将低序字节存储在起始地址(低地址)Big endian
大端字节序:将高序字节存储在起始地址(低地址)- 网络字节序是
big endian
的字节序,x86 系列 CPU 都是little-endian
的字节序,所以用 x86 的电脑向网络传输数据,先转换字节序
具体示例:在内存中双字0x01020304
(DWORD)的存储方式:
内存地址: | 低地址 | 高地址 | ||
---|---|---|---|---|
内存地址: | 4000 | 4001 | 4002 | 4003 |
LE | 0000 0100( 04 ) | 0000 0011( 03 ) | 0000 0010( 02 ) | 0000 0001( 01 ) |
BE | 0000 0001( 01 ) | 0000 0010( 02 ) | 0000 0011( 03 ) | 0000 0100( 04 ) |
2、字节序转换 api :
#include<arpa/inet.h>
uint16_t htons(uint16_t hostshort); //返回网络字节序的值
uint32_t htonl(uint32_t hostlong); //返回网络字节序的值
uint16_t ntohs(uint16_t netshort); //返回主机字节序的值
uint32_t ntohl(uint32_t netlong); //返回主机字节序的值
// h 代表 host ,n 代表 net,s 代表 short(2个字节),l 代表 long(4个字节)
三、socket 套接字网络编程
1、步骤:
2、函数原型:
① 创建套接字:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
返回值: | 成功返回套接字描述符,失败返回 -1 |
---|
domain 参数 |
协议族,指定所使用的协议族,通常为AF_INET ,表示互联网协议族(TCP/IP 协议族) |
---|---|
AF_INET |
IPv4 互联网协议 |
AF_INET6 |
IPv6 互联网协议 |
AF_UNIX |
Unix 域 |
AF_ROUTE |
路由套接字 |
AF_KEY |
密钥套接字 |
AF_UNSPEC |
未指定 |
more… |
type 参数 |
套接字类型描述 |
---|---|
SOCK_STREAM |
流式套接字提供可靠的、面向连接的通信流;它使用TCP 协议,从而保证了数据传输的正确性和顺序性 |
SOCK_DGRAM |
数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠的、无差错的。它使用数据报协议UDP |
SOCK_RAW |
允许程序使用底层协议,原始套接字允许对底层协议和IP 或ICMP 进行直接访问,功能强大但使用较为不方便,主要用于一些协议的开发 |
more… |
protocol 参数 |
套接口协议类型。通常赋 0 值,表示指定选择与type 参数对应的默认协议 |
---|---|
0 |
选择与type 参数对应的默认协议 |
IPPROTO_TCP |
TCP 传输协议 |
IPPROTO_UDP |
UDP 传输协议 |
IPPROTO_SCTP |
SCTP 传输协议 |
IPPROTO_TIPC |
TIPC 传输协议 |
more… |
② 为套接字添加信息( IP 地址和端口号):
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
返回值: | 成功返回 0,失败返回 -1 |
---|---|
sockfd 参数: |
要绑定的套接字描述符 |
addr 参数: |
是一个指向包含本机 IP 地址及端口号等信息的 sockaddr 类型的指针,指向要绑定给 sockfd 的协议地址结构,这个地址结构根据地址创建 socket 时的地址协议族的不同而不同 IPv4 对应的是: struct sockaddr{
sa_family_t sa_family; // 协议族char sa_data[14]; // IP + 端口}; 一般用 sockaddr_in 结构体同等替换 sockaddr 结构体: #include <netinet/in.h> // 或 #include <linux/in.h>struct sockaddr_in{
sa_family_t sin_family; // 协议族(常用 AF_INET)in_port_t sin_port; // 端口号,用户一般用 5000~9000,注意要转换为网络字节序再赋值struct in_addr sin_addr; // IP 地址结构体unsigned char sin_zero[8]; // 填充,没有实际意义,只是为跟 sockaddr 结构在内存中对齐,这样两者才能相互转换 }; IP 地址结构体如下:uint32_t s_addr 为 32 位二进制网络字节序的 IP 地址: struct in_addr{
uint32_t s_addr; // 按 32 位二进制网络字节序排列的 IP 地址,本机地址可用 ifconfig 查看,注意使用地址转换函数进行转换}; 地址转换函数: #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp); // 将 cp 指向的形如 192.168.x.xxx 的字符串 IP 地址转换为按网络字节序排列的 IP 地址,存放在 inp 指向的结构体中in_addr_t inet_addr(const char *cp); // 将 cp 指向的形如 192.168.x.xxx 的字符串 IP 地址转换为按网络字节序排列的 IP 地址,存放在返回的结构体中char *inet_ntoa(struct in_addr in); // 将结构体 in 中的网络字节序 IP 地址转换为形如 192.168.x.xxx 的字符串 IP 地址in_addr_t inet_network(const char *cp); struct in_addr inet_makeaddr(int net, int host); in_addr_t inet_lnaof(struct in_addr in); in_addr_t inet_netof(struct in_addr in); |
addrlen 参数: |
addr 参数指向的结构体的大小,可用sizeof() 计算 |
③ 监听被绑定的端口,监听网络连接请求:
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
- 设置能处理的最大连接数,
listen()
并未开始接受连线,只是设置sockect
的listen
模式,listen()
只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后响应该连接请求,并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。主要就两个功能:① 将一个未连接的套接字转换为一个被动套接字(监听);② 规定内核为相应套接字排队的最大连接数。- 内核为任何一个给定监听套接字维护两个队列:
① 未完成连接队列,每个这样的SYN
报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP
三次握手过程。这些套接字处于SYN_REVD
状态;
② 已完成连接队列,每个已完成TCP
三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED
状态
返回值: | 成功返回 0,失败返回 -1 |
---|---|
sockfd 参数: |
套接字描述符,监听该套接字绑定的端口 |
backlog 参数: |
指定在请求队列中允许的最大请求数 |
④ 连接指定计算机的端口,与服务器建立连接:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 建立与指定
socket
的连接,连接时阻塞,直到连接成功
返回值: | 成功返回 0,失败返回 -1 |
---|---|
sockfd 参数: |
socket 套接字描述符 |
addr 参数: |
包含目标服务器端IP 地址和端口号的结构体,sockaddr 结构体见bind() 函数addr 参数 |
addrlen 参数: |
addr 参数指向的结构体的长度,可用sizeof() 计算 |
⑤ 监听到有客户端接入,接受一个连接:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()
由TCP
服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(如果队列中没有连接请求,accept()
函数会一直等待,直到接收到连接请求才返回)- 一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字
- 已连接套接字(表示 TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭
返回值: | 该函数的返回值是一个新的套接字描述符,表示已连接套接字的描述符,而socket() 返回值是服务器监听套接字描述符 |
---|---|
sockfd 参数: |
socket 套接字描述符 |
addr 参数: |
用来返回已连接的对端(客户端)的协议地址,不关心时置NULL ,sockaddr 结构体见bind() 函数addr 参数 |
addrlen 参数: |
addr 参数指向的结构体的长度,可用sizeof() 计算,不关心时置NULL |
⑥ 数据收发常用 api:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
/* -------------------------------------------------------------------- */
#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);
// flags 参数为控制选项,一般可设置为 0
UDP 连接常用收发函数:
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
⑦ 关闭 socket 套接字,断开连接:
#include <unistd.h>
int close(int fd);
四、示例
Server:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
//#include<linux/in.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>