一,认识 IP 地址
IP协议有两个版本, IPv4和IPv6
IP地址是在IP协议中, 用来标识网络中不同主机的地址
对于IPv4来说, IP地址是一个4字节, 32位的整数
我们通常使用 “点分十进制” 的字符串表示IP地址, 例如 192.168.0.1
用点分割的每一个数字表示一个字节, 范围是 0 - 255
源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址
就好比唐僧西天取经,他总说“贫僧自东土大唐而来,前往西天拜佛求经”
这里的东土大唐就是 源IP地址
西天就是 目的IP地址
我们光有IP地址就可以完成通信了吗?
想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析,所以还需要一个端口号
端口号
端口号(port)是传输层协议的内容. 端口号是一个2字节16位的整数
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
一个端口号只能被一个进程占用
“端口号” 和 “进程ID”
pid 表示唯一一个进程
此处我们的端口号也是表示唯一一个进程
这两者之间是怎样的关系?
- 进程ID是由操作系统内核进行分配和管理的,而端口号是由通讯协议内核分配并进行管理的。
- 至于端口号和进程ID的对应关系则是由通讯协议在分配端口时记录进程ID,并维持一张对应的表进行管理
- 另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定
源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号
分别叫做源端口号和目的端口号
就是在描述 “数据是谁发的, 要发给谁”
2,传输层协议
认识 TCP 协议
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
认识 UDP 协议
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
3,网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分
磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分
网络数据流同样有大端小端之分
那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接收到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
大小端举例
- 例如要将数据0x1234abcd 写入以0x0000开始的内存中
为了使网络程序具有可移植性,使同样的C代码在大端和小端机器上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
htonl
//将主机数转换成无符号长整形的网络字节顺序
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
//hostlong:主机字节顺序表达的32位数
ntohl
uint32_t ntohl(uint32_t netlong);
//将一个无符号长整形数从网络字节顺序转换为主机字节顺序
//返回一个以主机字节顺序表达的数
htons
//将主机的无符号短整形数转换成网络字节顺序
uint16_t htons( uint16_t hostshort);
//hostshort:主机字节顺序表达的16位数
ntohs
//将一个无符号短整型数从网络字节顺序转换为主机字节顺序。
uint16_t ntohs(uint16_t netshort);
//netshort:一个以网络字节顺序表达的16位数
这些函数名很好记,h 表示host(主机),n 表示network(网络)
l 表示32位长整数,s 表示16位短整数
例如htonl
表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
4,socket编程接口
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
//如果函数调用成功,会返回一个标识这个套接字的文件描述符
//失败的时候返回-1
参数
1,domain
函数socket()的参数domain用于设置网络通信的域,函数socket()根据这个参数选择通信协议的族。通信协议族在文件sys/socket.h中定义。
domain的值及含义
名称 | 含义 | 名称 | 含义 |
---|---|---|---|
PF_UNIX,PF_LOCAL | 本地通信 | PF_X25 | ITU-T X25 / ISO-8208协议 |
AF_INET,PF_INET | IPv4 Internet协议 | PF_AX25 | Amateur radio AX.25 |
PF_INET6 | IPv6 Internet协议 | PF_ATMPVC | 原始ATM PVC访问 |
PF_IPX | IPX-Novell协议 | PF_APPLETALK | Appletalk |
PF_NETLINK | 内核用户界面设备 | PF_PACKET | 底层包访问 |
2,type
函数socket()的参数type用于设置套接字通信的类型,主要有SOCKET_STREAM(流式套接字)、SOCK——DGRAM(数据包套接字)等。
type的值及含义
名称 | 含义 |
---|---|
SOCK_STREAM | Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | 支持UDP连接(无连接状态的消息) |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
SOCK_RAW | RAW类型,提供原始网络协议访问 |
SOCK_RDM | 提供可靠的数据报文,不过可能数据会有乱序 |
SOCK_PACKET | 这是一个专用类型,不能呢过在通用程序中使用 |
3,protocol
函数socket()的第3个参数protocol用于指定某个协议的特定类型,即type类型中的某个类型。
通常某协议中只有一种特定类型,这样protocol参数设置为0
但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型
类型为SOCK_STREAM的套接字表示一个双向的字节流,与管道类似
流式的套接字在进行数据收发之前必须已经连接,连接使用connect()函数进行
一旦连接,可以使用read()或者write()函数进行数据的传输
流式通信方式保证数据不会丢失或者重复接收,当数据在一段时间内仍然没有接受完毕,可以认为这个连接已经死掉
SOCK_DGRAM
和SOCK_RAW
这两种套接字可以使用函数sendto()
来发送数据,使用recvfrom()
函数接受数据,recvfrom()接受来自指定IP地址的发送方的数据
SOCK_PACKET
是一种专用的数据包,它直接从设备驱动接受数据
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
socket:
标识一个未捆绑套接口的描述字。
address:
赋予套接口的地址。是一个指向sockaddr结构体类型的指针
sockaddr结构定义如下:
struct sockaddr{
u_short sa_family;
char sa_data[14];
};
address_len:
表示address结构的长度,可以用sizeof操作符获得。
返回值:成功返回0,失败返回-1
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
socket:
用于标识一个已捆绑未连接套接口的描述字。
backlog:
等待连接队列的最大长度。
为了接受连接,先用socket()创建一个套接口的描述字,然后用listen()创建套接口并为申请进入的连接建立一个后备日志,然后便可用accept()接受连接了。
listen()仅适用于支持连接的套接口,如SOCK_STREAM类型的。
套接口s处于一种“变动”模式,申请进入的连接请求被确认,并排队等待被接受。
这个函数特别适用于同时有多个连接请求的服务器;如果当一个连接请求到来时,队列已满,那么客户将收到一个WSAECONNREFUSED错误。
当没有可用的描述字时,listen()函数仍试图正常地工作。它仍接受请求直至队列变空。
当有可用描述字时,后续的一次listen()或accept()调用会将队列按照当前或最近的“后备日志”重新填充,如有可能的话,将恢复监听申请进入的连接请求。
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
参数
socket:
套接字描述符,该套接口在listen()后监听连接。
address:
(可选)指针,指向一缓冲区,其中接收为通讯层所知的连接实体的地址。此参数的实际格式由套接口创建时所产生的地址族确定。
address_len:
(可选)指针,输入参数,配合address一起使用,指向存有address地址长度的整型数。
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
参数:
sockfd:
标识一个未连接socket
addr:
指向要连接套接字的sockaddr结构体的指针
addrlen:
sockaddr结构体的字节长度
本函数用于创建与指定外部端口的连接。
sockfd 参数指定一个未连接的数据报或流类套接口。
如套接口未被捆绑,则系统赋给本地关联一个唯一的值,且设置套接口为已捆绑。
请注意若名字结构中的地址域为全零的话,则connect()将返回WSAEADDRNOTAVAIL
错误。
对于流类套接口(SOCK_STREAM
类型),利用名字来与一个远程主机建立连接,一旦套接口调用成功返回,它就能收发数据了。
对于数据报类套接口(SOCK_DGRAM
类型),则设置成一个缺省的目的地址,并用它来进行后续的send()
与recv()
调用。
IPv4和IPv6的地址格式定义在netinet/in.h
中
IPv4地址用sockaddr_in
结构体表示,包括16位地址类型, 16位端口号和32位IP地址
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
IPv4、IPv6地址类型分别定义为常数AF_INET
、AF_INET6
这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体
就可以根据地址类型字段确定结构体中的内容
socket API可以都用struct sockaddr *
类型表示, 在使用的时候需要强制转化成sockaddr_in;
这样的好处是程序的通用性好, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in;
这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
in_addr 结构
/* Internet address. */
struct in_addr {
__be32 s_addr;
};
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
5,简单的 UDP 网络程序
server
/*================================================================
# File Name: udp_server.c
# Author: rjm
# mail: rjm96@foxmail.com
# Created Time: 2018年05月03日 星期四 08时32分03秒
================================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
//socket的参数使用SOCK_DGRAM表示UDP
//bind之后就可以直接进行通信了
//使用sendto和recvfrom来进行数据读写
int main(int argc, char* argv[])
{
//1, 创建 socket 文件描述符
int sockfd = socket(AF_INET,SOCK_DGRAM,0);//AF_INET 表示ipv4协议
//SOCK_DGRAM 表示支持 udp 连接
//通常某协议中只有一种特定类型,这样protocol参数设置为0
if(sockfd < 0)
{
perror("socket");
return 1;
}
//初始化 sockaddr_in 结构体
struct sockaddr_in local;
local.sin_family = AF_INET; //ipv4协议
local.sin_port = htons(atoi(argv[2])); //第三个参数是端口号
local.sin_addr.s_addr = inet_addr(argv[1]); //第二个参数是ip地址
//inet_addr函数功能是将一个点分十进制的ip
//转换成一个u long型的整数
//2, 绑定端口号
if(bind(sockfd, (struct sockaddr*)&local, sizeof(local)) != 0)
{
perror("bind");
return 2;
}
//3, 收发数据
char buf[1024] = {0};
struct sockaddr_in client;
while(1)
{
socklen_t len = sizeof(client);
//函数原型:ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags,
// struct sockaddr *from,socket_t *fromlen);
//ssize_t 相当于 long int,socket_t 相当于int
//sockfd:标识一个已连接套接口的描述字。
//buf:接收数据缓冲区。
//len:缓冲区长度。
//flags:调用操作方式。
ssize_t s = recvfrom(sockfd, buf, sizeof(buf)-1, 0,
(struct sockaddr*)&client, &len);
if(s > 0)
{
//成功
buf[s] = 0; //将第一个设置为0, 以清空缓冲区
printf("[%s:%d]: %s\n", inet_ntoa(client.sin_addr), \
ntohs(client.sin_port), buf);
//inet_ntoa将一个十进制网络字节序转换为点分十进制IP格式的字符串
//int sendto ( socket s , const void * msg, int len,
// unsigned int flags,
// const struct sockaddr * to , int tolen ) ;
//sendto()用来将数据由指定的socket传给对方主机。
//参数s为已建好连线的socket
//如果利用UDP协议则不需经过连线操作
//参数msg指向欲连线的数据内容
//参数flags一般设0
//参数to用来指定欲传送的网络地址
//参数tolen为sockaddr的结构长度
sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr*)&client, sizeof(client));
//将接收到的数据返回给发送方
}
}
}
client
/*================================================================
# File Name: udp_client.c
# Author: rjm
# mail: rjm96@foxmail.com
# Created Time: 2018年05月03日 星期四 08时30分52秒
================================================================*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("Usage: ./udp_client [ip] [port]\n");
return 1;
}
//1, 创建 socket 文件描述符
int sockfd = socket(AF_INET,SOCK_DGRAM,0);//AF_INET 表示ipv4协议
//SOCK_DGRAM 表示支持 udp 连接
//通常某协议中只有一种特定类型,这样protocol参数设置为0
if(sockfd < 0)
{
perror("socket");
return 2;
}
//初始化 sockaddr_in 结构体
struct sockaddr_in server;
server.sin_family = AF_INET; //ipv4协议
server.sin_port = htons(atoi(argv[2])); //第三个参数是端口号
server.sin_addr.s_addr = inet_addr(argv[1]); //第二个参数是ip地址
//inet_addr函数功能是将一个点分十进制的ip
//转换成一个u long型的整数
char buf[1024] = {0};
struct sockaddr_in peer;
while(1)
{
socklen_t len = sizeof(peer);
printf("Please Enter: ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if(s > 0)
{
//成功
buf[s-1] = 0; //将第一个设置为0, 以清空缓冲区, -1是因为read会把\n读进来, -1把\n减去
//int sendto ( socket s , const void * msg, int len, unsigned int flags,
// const struct sockaddr * to , int tolen ) ;
//sendto()用来将数据由指定的socket传给对方主机。
//参数s为已建好连线的socket
//如果利用UDP协议则不需经过连线操作
//参数msg指向欲连线的数据内容
//参数flags一般设0
//参数to用来指定欲传送的网络地址
//参数tolen为sockaddr的结构长度
sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&server, sizeof(server));
ssize_t _s = recvfrom(sockfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&peer, &len);
if(_s > 0)
{
buf[_s] = 0;
printf("server echo: %s\n", buf);
}
}
}
}
效果
如果有多台主机,连接在同一个局域网里,就可以实现不同主机之间的通信了