网络通信:全双工
TCP和UDP
相同点:同属传输层
区别:
TCP:有连接:通信双方在通信之前事先建立连接,类似于打电话。
整个通信过程可以保证可靠的传输,即数据不会丢包、失序、乱码等,TCP在传输过程中还可实现流量控制。
使用场合:可靠新要求比较高的场合,比如账户密码、文件传输等。
UDP:无连接,类似于寄信。
整个通信过程中数据包可能会出现丢包和失序等,也没有流量控制。
适用场合:视频通信、及时聊天等。
流式套接字(SOCK_STREAM):基于TCP协议通信的套接字
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM):基于UDP协议的套接字
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW):不经过传输层,故无默认的tcp层协议。
可以对较低层次协议如IP、ICMP直接访问。
端口号:为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区别
1——1023已经固定的用于某些通信服务,用户不能复用。
编程API:
一、TCP服务器端通信模型:
1.创建套接字
int socket (int domain, int type, int protocol);
功能:创建通信套接字
参数:
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 | 底层包访问 |
type:指明通信套接字类型(SOCK_STREAM, SOCK_DGRAM, SOCK_RAW)
type的值及含义:
名称 | 含义 |
---|---|
SOCK_STREAM | Tcp连接,提供序列化的、可靠的、双向连接的字节流。支持带外数据传输 |
SOCK_DGRAM | 支持UDP连接(无连接状态的消息) |
SOCK_SEQPACKET | 序列化包,提供一个序列化的、可靠的、双向的基本连接的数据传输通道,数据长度定常。每次调用读系统调用时数据需要将全部数据读出 |
SOCK_RAW | RAW类型,提供原始网络协议访问 |
SOCK_RDM | 提供可靠的数据报文,不过可能数据会有乱序 |
SOCK_PACKET | 这是一个专用类型,不能呢过在通用程序中使用 |
并不是所有的协议族都实现了这些协议类型,例如,AF_INET协议族就没有实现SOCK_SEQPACKET协议类型。
protocol:用于指定某个协议的特定类型,即type类型中的某个类型。通常某协议中只有一种特定类型,这样protocol参数仅能设置为0;但是有些协议有多种特定的类型,就需要设置这个参数来选择特定的类型。
返回值:
成功:新创建的套接字文件描述符
失败:-1
2.为套接字绑定本地地址信息
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
功能:为本地套接字绑定地址信息
参数:
sockfd:指明需要绑定的本地套接字
my_addr:指向含有地址信息的结构体空间
addrlen:表明绑定的地址信息长度
返回值:
成功:0
失败:-1
internet通信地址信息:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */——>需要进行字节序转换
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */——>需要进行字节序转换
};
字节序转换:
主机字节序转网络字节序:
u_long htonl (u_long hostlong);
u_short htons (u_short short);
网络字节序到主机字节序
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);
IP地址转换:
inet_aton()
将strptr所指的字符串转换成32位的网络字节序二进制值
#include <arpa/inet.h>
int inet_aton(const char *strptr,struct in_addr *addrptr);
inet_addr()
功能同上,返回转换后的地址。
in_addr_t inet_addr(const char *strptr);
inet_ntoa()
将32位网络字节序二进制地址转换成点分十进制的字符串。
char *inet_ntoa(stuct in_addr inaddr);
建立连接:
3.监听
int listen (int sockfd, int backlog);
功能:为套接字sockfd创建监听队列,用于服务器端。
参数:
sockfd:本地套接字
backlog:监听队列大小
返回值:
成功:0
失败:-1
4.接收客户端连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接受客户端的连接请求
参数:
sockfd:本地套接字
addr:指向存储客户端地址信息的结构体空间
addrlen:(值结果参数)指明客户端地址信息存储长度
返回值:
成功:返回已经建立连接的套接字文件描述符
失败:-1
收发消息:
5.收发消息
ssize_t send(int socket, const void *buffer, size_t length, int flags);
功能:发送消息
参数:
socket:本地套接字
buffer:要发送的消息的首地址
length:指明消息长度
flags:发送消息的标志位,默认0
返回值:
成功:成功发送到内核发送缓冲区的字节数,不能用于表示对方成功接收的字节数。(tcp内部存在一个发送缓冲区)
失败:-1
ssize_t recv(int socket, void *buffer, size_t length, int flags);
功能:从本地套接字接收消息
参数:
socket:本地套接字
buffer:指明收到的消息的存储首地址。
length:指明接收消息长度
flags:指明接收方式
返回值:
成功:返回成功从内核接收缓冲区拷贝到buffer的字节数,返回0表示对方发送端关闭。
失败:返回-1
6.关闭不用的套接字
int close(int sockfd);
二、TCP客户端通信模型
1.创建套接字
2.绑定本地地址信息(可选)
3.向服务器发起连接请求
int?connect(int?sockfd,?struct?sockaddr?*serv_addr, int?addrlen);
功能:向对方发起连接请求
参数:
sockfd:指明本地套接字
serv_addr:指明对方的地址信息
addrlen:指明地址信息的长度
返回值:
成功:0
失败:-1
4.收发消息
5.关闭不用的套接字
什么是粘包,为什么会出现粘包,UDP有粘包吗?
(1)发送方原因
我们知道,TCP默认会使用Nagle算法。而Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。
所以,正是Nagle算法造成了发送方有可能造成粘包现象。
(2)接收方原因
TCP接收到分组时,并不会立刻送至应用层处理,或者说,应用层并不一定会立即处理;实际上,TCP将收到的分组保存至接收缓存里,然后应用程序主动从缓存里读收到的分组。这样一来,如果TCP接收分组的速度大于应用程序读分组的速度,多个包就会被存至缓存,应用程序读时,就会读到多个首尾相接粘到一起的包。
UDP不存在粘包问题,是由于UDP发送的时候,没有经过Negal算法优化,不会将多个小包合并一次发送出去。另外,在UDP协议的接收端,采用了链式结构来记录每一个到达的UDP包,这样接收端应用程序一次recv只能从socket接收缓冲区中读出一个数据包。也就是说,发送端send了几次,接收端必须recv几次(无论recv时指定了多大的缓冲区)。
粘包怎么解决?
对于发送方造成的粘包现象,我们可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭Nagle算法。(但是没有接收方的处理方法)
应用层的处理简单易行!并且不仅可以解决接收方造成的粘包问题,还能解决发送方造成的粘包问题。
解决方法就是循环处理:应用程序在处理从缓存读来的分组时,读完一条数据时,就应该循环读下一条数据,直到所有的数据都被处理;但是如何判断每条数据的长度呢?
两种途径:
1)格式化数据:每条数据有固定的格式(开始符、结束符),这种方法简单易行,但选择开始符和结束符的时候一定要注意每条数据的内部一定不能出现开始符或结束符;
2)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。