一、TCP和UDP对比以及应用场景分析
- TCP面向连接
- TCP需要建立连接,UDP无连接。 所以UDP快,适用于实时性要求高的场景,而TCP安全。
- 并且由于UDP无连接,所以支持多对多通信,而TCP只能一对一通信。
- TCP可靠性传输,UDP不可靠
- TCP有三次握手和四次挥手、序号、确认应答、超时重传、滑动窗口以及拥塞控制等机制来保证可靠性传输,UDP没有这些机制。所以UDP在传输量小的情况下快,TCP可靠性传输并适用于大量数据传输的情况。
- TCP这些机制需要在报文首部中设置SYN、ACK等数据,增大了首部开销(首部20字节),UDP只需要8字节
- TCP面向字节流传输,UDP面向报文传输
- 面向字节流是以字节为单位发送数据,并且一个数据包可以以字节大小来拆分成多个数据包,以方便发送。TCP可以先将数据存放在发送缓冲区中,可以等待数据到一定大小发送,也可以直接发送,没有固定大小可言。UDP需要每次发送都需要发送固定长度的数据包,如果长度过长需要应用层协议主动将其裁剪到适合长度。
所以TCP一般用在需要传输大量数据且对可靠性要求高的情况下,如HTTP;而UDP一般适用于对实时性要求高的场景,如直播、游戏、语音通话等
二、TCP和UDP编程过程
TCP
- TCP 服务器端–主要步骤
1.建立监听socket //socket()
2.监听socket绑定ip地址和端口 //bind()
3.进行监听 //listen()
//接收到syn报文后协议栈负责三次握手
4.三次握手结束后为全连接中的tcp控制块分配fd,即建立与客户端对话的socket //accept()
5.传输数据 //send(), recv()
6.断开连接 //close()
//协议栈负责四次挥手
- TCP 客户端–主要步骤
1.建立与服务器对话的客户端socket //socket()
2.客户端socket绑定ip地址和端口(可选) //bind()
3.根据服务器的地址和端口进行连接 //connect()
//协议栈发送syn报文,进行三次握手
4.传输数据 //send(), recv()
5.断开连接 //close()
//协议栈负责四次挥手
UDP
- UDP --主要步骤
1.创建一个监听socket //socket()
2.设置监听socket属性(可选) //setsockopt()
3.监听socket绑定ip地址和端口 //bind()
4.接收和发送数据 //recvfrom(), sendto()
5.关闭网络连接 //close()
三、函数详解
1.setsockopt()
端口复用允许在一个应用程序可以把 n 个套接字绑在一个端口上而不出错
extern int setsockopt (int __fd, int __level, int __optname,
const void *__optval, socklen_t __optlen) __THROW;
int __fd : 被设置的socket
int __level : 选项定义的层次;支持SOL_SOCKET、IPPROTO_TCP、IPPROTO_IP和IPPROTO_IPV6
SOL_SOCKET : 通用套接字选项.
IPPROTO_IP : IP选项.IPv4套接口
IPPROTO_TCP : TCP选项.
IPPROTO_IPV6 : IPv6套接口
int __optname : 欲设置的选项,有下列几种数值
当level为SOL_SOCKET时,optname可以有以下选项(一部分)
选项名称 说明 数据类型
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SOREUSEADDR 允许重用本地地址 int
SOREUSEPORT 允许重用端口 int
const void *__optval : 指针,指向存放选项待设置的新值的缓冲区,可以指向普通变量或结构体
获得或者是设置套接字选项.根据选项名称的数据类型进行转换
socklen_t __optlen : optval缓冲区长度
- sendto()
extern ssize_t sendto (int __fd, const void *__buf, size_t __n,
int __flags, __CONST_SOCKADDR_ARG __addr,
socklen_t __addr_len);
int __fd : socket文件描述符
const void *__buf : 发送数据的首地址
size_t __n : 发送数据的长度
int __flags : 0, 默认方式发送数据,fags还可以设为以下标志的组合
MSG OOB, 发送带外数据
MSG_DONTROUTE, 告诉IP协议,目的主机在本地网络,没有必要查找路由表
MSGDONTWAI, 设置为非阳塞操作
MSGNOSIGNAL, 表示发送动作不愿被SIGPIPE信号中断
__CONST_SOCKADDR_ARG __addr : 目的主机的地址和端口
socklen_t __addr_len : __addr的长度
- recvfrom()
extern ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n,
int __flags, __SOCKADDR_ARG __addr,
socklen_t *__restrict __addr_len);
int __fd : socket文件描述符
void *__restrict__ __buf : 接收数据的首地址
size_t __n : 可接受数据的最大长度
int __flags : 0, 默认方式接收数据,flags还可以设为以下标志的组合
MSG OOB, 接收带外数据
MSG PEEK, 查看数据标志,返回的数据并不在系统中删除,如果再次调用recv通数会返回相同的数据内容
MsG DONTAT, 设置为非阴塞操作
MSG WAITALL, 强迫接收到en大小的数据后才返回,除非有错误或有信号产生
__SOCKADDR_ARG __addr : 发送方的IP地址和端口
socklen_t *__restrict __addr_len : __addr的长度
四、完整代码
服务器端
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
int main(){
//创建socket
int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
//定义地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6666);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定
if(bind(socketfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))==-1){
perror("bind error");
return -1;
}
//接受数据
char buffer[1024] = {0};
struct sockaddr_in recv_addr;
socklen_t recvlen = sizeof(recv_addr);
int recv_size = recvfrom(socketfd, buffer, 1024, 0, (struct sockaddr *)&recv_addr, &recvlen);
if(recv_size == -1){
printf("recv erro\n");
}
printf("recv from client : %s\n", buffer);
//发送数据
char message[1024] = {"this is server!"};
sendto(socketfd, message, 1024, 0, (struct sockaddr*)&recv_addr, sizeof(recv_addr));
close(socketfd);
}
客户端
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
int main(){
//创建socket,返回文件描述符
int socketfd = socket(AF_INET, SOCK_DGRAM, 0);
//定义地址
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//绑定地址
if(bind(socketfd, (struct sockaddr*)&server_addr, sizeof(struct sockaddr))==-1){
perror("bind error");
return -1;
}
//定义目标地址
struct sockaddr_in target_addr;
memset(&target_addr, 0, sizeof(sockaddr_in));
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(6666);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//发送数据
char buffer[1024] = {"this is client!"};
sendto(socketfd, buffer, 1024, 0, (struct sockaddr*)&target_addr, sizeof(target_addr));
//接受数据
struct sockaddr_in recv_addr;
socklen_t recvlen = sizeof(recv_addr);
int recv_size = recvfrom(socketfd, buffer, 1024, 0, (struct sockaddr *)&recv_addr, &recvlen);
if(recv_size == -1){
printf("recv erro\n");
}
printf("recv from server : %s\n", buffer);
close(socketfd);
}
备注
这么写需要服务器端先启动,不然客户端发送的时候服务端还没启动就会丢失数据然后阻塞在recvfrom
参考资源:https://blog.csdn.net/JMW1407/article/details/107321853