一、概述
TCP(传输控制协议)和UDP(用户数据报协议是网络体系结TCP/IP模型中传输层一层中的两个不同的通信协议。
TCP:传输控制协议,一种面向连接的协议,给用户进程提供可靠的全双工的字节流,TCP套接口是字节流套接口(stream socket)的一种。
UDP:用户数据报协议。UDP是一种无连接协议。UDP套接口是数据报套接口(datagram socket)的一种。
二、基于udp协议的socket客户端与服务端通信编程:
aa1 int socket(int domain,int type,int protocol);//创建套接字
int bind(int sockfd,const struct sockaddr *addr,socklen_t *addrlen); //为套接字绑定地址信息
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,sockaddr *src_addr,socklen_t *addrlen);
ssize_t sendto(int sockfd,void *buf,size_t len,int flags,struct sockaddr *dest_addr,socklen_t len);
int close(int fd); //关闭套接字
服务端: 1.创建套接字 在内核中创建struct socket结构体,使进程与网卡之间建立联系
int socket(domain,type,proto)
domain: 地址域 AF_INET–IPV4 地址域
type: 套接字类型
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字
proto:传输层协议类型
0-默认
IPPROTO_TCP 6
IPPRPTO_UDP 17
返回值:套接字操作句柄–文件描述符
2.为套接字绑定地址信息
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
在socket结构体中描述符这个socket处理哪个地址和端口的数据
服务端绑定地址的目的:告诉操作系统网卡接收到数据的时候,哪个地址和端口的数据应该放到这个socket的接收队列中
bind绑定只能绑定本机存在的地址
客户端:2.为套接字绑定地址信息;发送数据的时候能够表述数据从哪个地址端口发送出去,对方回复数据就会回复到这个地址端口上,
3.发送数据 sendto(sockfd,data,dlen,flag,dest_addr,addr_len)
将data中的dlen长度的数据通过sockfd对应的socket结构中的ip/端口将数据发送到dest_addr地址的主机上
4.接收数据
recvfrom(sockfd,buf,len,flag,&peer_addr,&addr_len)
从sockfd对应的socket结构体中的接收队列中取出一条数据放到buf中;
使用c++封装一个udpsocket类,来实现socket的简单操作
class UdpSocket{
private:
int_sockfd;
public:
bool Socket();
bool Bind(const std::string &ip,const uint16_t port);
bool Send(const std::string &data,const std::string &peer_ip,const uint16_tpeer_port);
bool Recv(std::string &buf,std::string &peer_ip,uint16_t &peer_port);
void Close();
}
UDP类的封装:
/*===============================================================
* Copyright (C) . All rights reserved.")
* 文件名称:
* 创 建 者:zhang
* 创建日期:
* 描 述:封装UdpSocket类,实例化对象,向外提供简单的socket接口
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 发送数据
* 4. 接收数据
* 5. 关闭套接字
================================================================*/
#include
#include
#include
#include
#include
#include
#define CHECK_RET(q) if((q)==false){return -1;}
class UdpSocket{
private:
int _sockfd;
public:
bool Socket() {
//int socket(int domain, int type, int protocol);
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (_sockfd < 0) {
std::cerr << "socket error\n";
return false;
}
return true;
}
bool Bind(const std::string &ip, const uint16_t port) {
//bind(int sockfd, struct sockaddr *addr,socklen_t addrlen)
struct sockaddr_in addr;
addr.sin_family = AF_INET;
//uint16_t htons(uint16_t hostshort);
//将主机字节序的16位数据,转换位网络字节序数据返回
addr.sin_port = htons(9000);
//192.168.122.132 -> 0xc0a87a84
//in_addr_t inet_addr(const char *cp);
//将点分十进制字符串IP地址转换为网络字节序IP地址
addr.sin_addr.s_addr = inet_addr(ip.c_str());
int ret;
socklen_t len = sizeof(struct sockaddr_in);
ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
std::cerr << "bind error\n";
return false;
}
return true;
}
bool Send(const std::string &data, const std::string &peer_ip,
const uint16_t peer_port) {
//ssize_t sendto(int sockfd, const void *buf, size_t len,
//int flags,struct sockaddr *dest_addr, socklen_t addrlen);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(peer_port);
addr.sin_addr.s_addr = inet_addr(peer_ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, &data[0], data.size(), 0,
(struct sockaddr*) &addr, len);
if (ret < 0) {
std::cerr << "sendto error\n";
return false;
}
return true;
}
bool Recv(std::string &buf, std::string &peer_ip,
uint16_t &peer_port) {
//ssize_t recvfrom(int sockfd, void *buf, size_t len,
//int flags,struct sockaddr *src_addr, socklen_t *addrlen)
//成功:返回实际接收的数据长度 , 失败:-1
struct sockaddr_in peer_addr;
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0,
(struct sockaddr*)&peer_addr, &len);
if (ret < 0) {
std::cerr << "recvfrom error\n";
return false;
}
//char *inet_ntoa(struct in_addr in);
//将网络字节序IP地址转换为点分十进制字符串IP地址
//uint16_t ntohs(uint16_t netshort);
//将网络字节序的16位数据转换为主机字节序数据
peer_ip = inet_ntoa(peer_addr.sin_addr);
peer_port = ntohs(peer_addr.sin_port);
buf.assign(tmp, ret);
return true;
}
void Close() {
close(_sockfd);
}
};
UDP服务端代码:
#include "udpsocket.hpp"
#include
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./udp_srv 192.168.122.132 9000\n";
return -1;
}
uint16_t port;
std::string ip = argv[1];
std::stringstream tmp;
tmp << argv[2];
tmp >> port;
UdpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip, port));
while(1) {
std::string buf;
std::string peer_ip;
uint16_t peer_port;
sock.Recv(buf, peer_ip, peer_port);
std::cout << "client-["<> buf;
sock.Send(buf, peer_ip, peer_port);
}
sock.Close();
}
UDP客户端代码:
#include "udpsocket.hpp"
#include
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./udp_cli ip port\n";
return -1;
}
uint16_t port;
std::string ip = argv[1];
std::stringstream tmp;
tmp << argv[2];
tmp >> port;
UdpSocket sock;
CHECK_RET(sock.Socket());
//客户端不推荐用户主动绑定固定地址,因为一个端口只能被一个进程占用
//因此一旦端口固定,这个客户端程序就只能启动一个
while(1) {
std::string buf;
std::cin >> buf;
//当socket还没有绑定地址,这时候操作系统在发送之前可以检测到
//这时候操作系统会为socket选择一个合适的地址和端口进行绑定
sock.Send(buf, ip, port);
buf.clear();
sock.Recv(buf, ip, port);
std::cout << "server say:" << buf << std::endl;
}
sock.Close();
return 0;
}
三、基于tcp协议的客户端与服务端通讯流程:面向连接,可靠传输,面向字节流
客户端: 服务端:
tcp客户端/服务端通信流程实现
class TcpSocket
{
private:
int _sockfd;
public:
bool Socket();
bool Bind(std::string &ip,std::string &port);
bool Listen(int backlog=5);
bool Connect(std::string &srv_ip,std::string &srv_port);
bool Accept(TcpSocket &clisock,std::string *ip=NULL,uint16_t *port=NULL);
bool Send(std::string &data);
bool Recv(std::string &buf);
bool Close();
};
当前服务端程序只能于一个客户端通信一次;
原因:因为服务端不知道客户端的新连接请求/数据什么时候到来,因此在程序流程写死的情况,就会阻塞在recv或者accpt两个接口处,导致流程无法继续
解决方案:服务端为每一个新的客户端都创建一个进程/线程来与客户端进行通信
连接断开的体现:当通信双方连接断开时
recv返回0(recv默认没有数据则阻塞)–表示连接断开,应该关闭套接字
send触发异常—SIGPIPE信号,会导致进程退出
tcp服务端缺陷:
tcp服务端为每个客户端都新建了套接字进行独立通信,但是服务端无法获知哪个客户端数据会先到来,因此可能会阻塞在等待连接请求或者等待接收某个客户端数据这里。
解决方案: 多线程/多进程任务处理
每个线程/进程独立负责一个功能
一个线程/进程复制客户端已完成连接获取功能
为每个客户端都新建一个线程/进程处理独立通信。
实现代码:
TCP类的封装:
/*===============================================================
* Copyright (C) . All rights reserved.")
* 文件名称:
* 创 建 者:zhang
* 创建日期:
* 描 述:z封装一个tcpsocket类,向外提供简单的套接字接口
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 向服务端发起连接请求
* 5. 服务端获取新建连接
* 6. 发送数据
* 7. 接收数据
* 8. 关闭套接字
================================================================*/
#include
#include
#include
#include
#include
#include
#include
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket
{
private:
int _sockfd;
public:
void SetFd(int fd) {
_sockfd = fd;
}
int GetFd() {
return _sockfd;
}
bool Socket() {
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (_sockfd < 0) {
std::cerr << "socket error\n";
return false;
}
return true;
}
int str2int(const std::string &str){
int num;
std::stringstream tmp;
tmp << str;
tmp >> num;
return num;
}
bool Bind(const std::string &ip, const std::string &port) {
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(str2int(port));
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
std::cerr << "bind error\n";
return false;
}
return true;
}
bool Listen(const int backlog = 5) {
//int listen(int sockfd, int backlog);
//开始监听:通知操作系统,可以开始接收客户端的连接请求了,
//并且完成三次握手建立连接过程
//tcp的面向连接,有一个三次握手建立连接过程
//backlog:客户端最大并发连接数(同一时间最多接收多少个客户端
//新连接请求)
int ret = listen(_sockfd, backlog);
if (ret < 0) {
std::cerr << "listen error\n";
return false;
}
return true;
}
bool Connect(const std::string &srv_ip,
const std::string &srv_port) {
//int connect(int sockfd, sockaddr *addr,socklen_t addrlen)
//addr: 服务端地址信息
//addrlen: 地址信息长度
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(str2int(srv_port));
addr.sin_addr.s_addr = inet_addr(srv_ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = connect(_sockfd, (struct sockaddr*)&addr, len);
if (ret < 0) {
std::cerr << "connect error\n";
return false;
}
return true;
}
bool Accept(TcpSocket &clisock, std::string *ip = NULL,
uint16_t *port = NULL) {
//int accept(int sockfd, sockaddr *addr, socklen_t *addrlen)
//sockfd: 监听套接字描述符
//addr: 客户端地址信息
//addrlen: 地址信息长度
//返回值:返回新建连接的socket描述符-与客户端进行数据通信
struct sockaddr_in cliaddr;
socklen_t len = sizeof(struct sockaddr_in);
int newfd = accept(_sockfd, (sockaddr*)&cliaddr, &len);
if (newfd < 0) {
std::cerr << "accept error\n";
return false;
}
clisock.SetFd(newfd);
if (ip != NULL) {
*ip = inet_ntoa(cliaddr.sin_addr);
}
if (port != NULL) {
*port = ntohs(cliaddr.sin_port);
}
return true;
}
bool Send(std::string &data) {
//ssize_t send(int sockfd, void *buf, size_t len, int flags)
//sockfd: 套接字描述符(服务端是新建连接的socket描述符)
//buf: 要发送的数据
//len: 要发送的数据长度
//flags: 0-默认阻塞发送
//返回值: 成功-返回实际发送的数据长度;失败-返回-1
int ret = send(_sockfd, &data[0], data.size(), 0);
if (ret < 0) {
std::cerr << "send error\n";
return false;
}
return true;
}
bool Recv(std::string &buf) {
//ssize_t recv(int sockfd, void *buf, size_t len, int flags)
//flags:
// 0-默认阻塞接收
// MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
//返回值:>0:实际接收的数据长度 ==0:连接断开 <0:错误
char tmp[4096];
int ret = recv(_sockfd, tmp, 4096, 0);
if (ret < 0) {
std::cerr << "recv error\n";
return false;
}else if (ret == 0) {
std::cerr << "connect shutdown\n";
return false;
}
buf.assign(tmp, ret);
return true;
}
bool Close() {
close(_sockfd);
}
};
TCP服务端代码:
/*===============================================================
* Copyright (C) . All rights reserved.")
* 文件名称:
* 创 建 者:zhang
* 创建日期:
* 描 述:tcp服务端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 获取已完成连接socket
* 5. 通过获取的新建socket与客户端进行通信-接收数据
* 6. 发送数据
* 7. 关闭套接字
================================================================*/
#include
#include "tcpsocket.hpp"
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_srv ip port\n";
return -1;
}
TcpSocket lst_sock;
/*1. 创建套接字*/
CHECK_RET(lst_sock.Socket());
/*2. 为套接字绑定地址信息*/
CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
/*3. 开始监听*/
CHECK_RET(lst_sock.Listen());
while(1){
/*4. 获取已完成连接socket*/
TcpSocket clisock;
bool ret = lst_sock.Accept(clisock);
if (ret == false) {
continue;
}
/*5. 通过获取的新建socket与客户端进行通信-接收数据*/
std::string buf;
ret = clisock.Recv(buf);
if (ret == false) {
clisock.Close();
continue;
}
std::cout << "client say: " << buf << std::endl;
/*6. 发送数据*/
buf.clear();
std::cout << "server say: ";
fflush(stdout);
std::cin >> buf;
clisock.Send(buf);
}
/*7. 关闭套接字 */
lst_sock.Close();
return 0;
}
TCP客户端代码:
/*===============================================================
* Copyright (C) . All rights reserved.")
* 文件名称:
* 创 建 者:zhang
* 创建日期:
* 描 述:tcp客户端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息(不推荐用户主动绑定)
* 3. 向服务端发起连接请求
* 4. 发送数据
* 5. 接收数据
* 6. 关闭套接字
================================================================*/
#include
#include
#include "tcpsocket.hpp"
void sigcb(int signo)
{
printf("recv a signo SIGPIPE --- conect shutdown\n");
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_cli ip port\n";
return -1;
}
signal(SIGPIPE, sigcb);
TcpSocket sock;
/*1. 创建套接字*/
CHECK_RET(sock.Socket());
/*2. 为套接字绑定地址信息(不推荐用户主动绑定)*/
/*3. 向服务端发起连接请求*/
CHECK_RET(sock.Connect(argv[1], argv[2]));
while(1) {
/*4. 发送数据*/
std::string buf;
std::cout << "client say: ";
fflush(stdout);
std::cin >> buf;
sock.Send(buf);
/*5. 接收数据*/
buf.clear();
sock.Recv(buf);
std::cout << "server say: " << buf << std::endl;
}
/*6. 关闭套接字 */
sock.Close();
return 0;
}
TCP进程实现:
/*===============================================================
* Copyright (C) . All rights reserved.")
* 文件名称:
* 创 建 者:zhang
* 创建日期:
* 描 述:tcp服务端通信流程
* 1. 创建套接字
* 2. 为套接字绑定地址信息
* 3. 开始监听
* 4. 获取已完成连接socket
* 5. 通过获取的新建socket与客户端进行通信-接收数据
* 6. 发送数据
* 7. 关闭套接字
================================================================*/
#include
#include
#include
#include "tcpsocket.hpp"
void sigcb(int no) {
//如果有僵尸进程可以处理,就一直处理
//如果没有子进程退出了则waitpid返回0,退出循环
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
if (argc != 3) {
std::cerr << "./tcp_srv ip port\n";
return -1;
}
signal(SIGCHLD, sigcb);
TcpSocket lst_sock;
/*1. 创建套接字*/
CHECK_RET(lst_sock.Socket());
/*2. 为套接字绑定地址信息*/
CHECK_RET(lst_sock.Bind(argv[1], argv[2]));
/*3. 开始监听*/
CHECK_RET(lst_sock.Listen());
while(1){
/*4. 获取已完成连接socket*/
TcpSocket clisock;
bool ret = lst_sock.Accept(clisock);
if (ret == false) {
continue;
}
if (fork() == 0) {
while(1) {
/*5. 通过获取的新建socket与客户端进行通信-接收数据*/
std::string buf;
clisock.Recv(buf);
std::cout << "client say: " << buf << std::endl;
/*6. 发送数据*/
buf.clear();
std::cout << "server say: ";
fflush(stdout);
std::cin >> buf;
clisock.Send(buf);
}
clisock.Close();
}
clisock.Close();
}
/*7. 关闭套接字 */
lst_sock.Close();
return 0;
}
四、对比tcp/udp协议,对比两者优缺点。
TCP优缺点:
优点:
1.TCP提供以认可的方式显式地创建和终止连接。
2.TCP保证可靠的、顺序的(数据包以发送的顺序接收)以及不会重复的数据传输。
3.TCP处理流控制。
4.允许数据优先
5.如果数据没有传送到,则TCP套接口返回一个出错状态条件。
6.TCP通过保持连续并将数据块分成更小的分片来处理大数据块。—无需程序员知道
缺点: TCP在转移数据时必须创建(并保持)一个连接。这个连接给通信进程增加了开销,让它比UDP速度要慢。
UDP优缺点:
1.UDP不要求保持一个连接
2.UDP没有因接收方认可收到数据包(或者当数据包没有正确抵达而自动重传)而带来的开销。
3.设计UDP的目的是用于短应用和控制消息
4.在一个数据包连接一个数据包的基础上,UDP要求的网络带宽比TDP更小。