socket套接字编程:网络通信程序的编写(接口+流程)
udp协议:用户数据报协议
特性:无连接,不可靠,面向数据报
应用场景:实时性要求大于安全性要求的场景---视频传输
tcp协议:传输控制协议
特性:面向连接,可靠传输,面向字节流
应用场景:安全性要求大于实时性要求的场景---文件传输
在网络通信程序中,通信两端被分为:客户端,服务器端
客户端:通常是提供给客户的通信端,通常是编写通信程序中主动发起请求的一端
服务端:通常指被动接受请求,提供服务的通信端
客户端必须提前能够知道服务端的地址信息(ip+port)是多少,服务端的地址信息通常是固定的,并且是提前提供给客户端的
字节序转换接口
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);32位数据主机到网络字节序转换
uint16_t htons(uint16_t hostshort);16位数据主机到网络字节序转换
uint32_t ntohl(uint32_t netlong);32位数据网络到主机字节序转换
uint16_t ntohs(uint16_t netshort);16位数据网络到主机字节序转换
port端口转换使用s,ip转换用l,不能混用
将字符串点分十进制IP地址转换为整型网络字节序IP地址(“192.168.2.2” -> 0xc0a80202)
in_addr_t inet_addr(const char* cp)
将网络字节序IP地址转换为字符串点分十进制IP地址
char* inet_ntoa(struct in_addr in) in.s_addr = 0xc0a80202
netstat命令:查看当前的网络状态信息
-a:查看所有
-t:查看tcp信息
-u:查看udp信息
-n:不以服务名称显示
-p:查看当前网络状态对应的进程
udp通信程序的编写:套接字接口
server通信流程:
(1)创建套接字(在内核中创建一个socket结构体)
(2)为套接字绑定地址信息,在创建套接字创建的socket结构体中加入IP+port信息(告诉操作系统主机收到的哪些数据应该交给当前的这个socket的接收缓冲区中,确定发送数据的源端地址信息)
(3)接收数据(当前进程从指定的socket接收缓冲区中取出数据)
(4)发送数据(将要发送的数据放到socket发送缓冲区中,内核选择合适时候封装发送)
(5)关闭套接字
client通信流程:
(1)创建套接字
(2)为套接字绑定地址(大多数情况下会忽略第2步,在发送数据时若socket没有绑定地址,则系统会选择合适的地址进行绑定)
(3)发送数据
(4)接收数据
(5)关闭套接字
接口认识:
(1)创建套接字:int socket(int domain, int type, int protocol)
domain:地址域类型---AF_INET(IPv4通信,使用IPv4的地址结构)
type:套接字类型---SOCK_STREAM(tcp通信使用) / SOCK_DGRAM(udp通信使用)
protocol:本次通信协议---IPPROTO_TCP(6) / IPPROTO_UDP(17) / 0(使用默认协议)
返回值:返回一个文件描述符---操作句柄,失败返回-1
(2)为套接字绑定地址信息
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
sockfd:创建套接字返回的操作句柄
addr:当前绑定的地址信息
socklen_t:地址信息长度
返回值:成功返回0,失败返回-1
(3)接收数据
ssize_t recvfrom(int sockfd, void* buf, int len, int flag, struct sockaddr* srcaddr, socklen_t* addrlen)
sockfd:操作句柄
buf:空间地址,用于存放接收的数据
len:要接收的数据长度
flag:选项标志---默认0,表示阻塞接收
srcaddr:获取本条数据的源端地址信息
addrlen:输入输出参数-指定要接收多长的地址结构,并且返回实际接收的地址长度
返回值:返回实际接收到的数据长度,失败返回-1
(4)发送数据
ssize_t sendto(int sockfd, void* data, int len, int flag, struct sockaddr* peeraddr, socklen_t addrlen)
sockfd:操作句柄
data:要发送的数据的空间首地址
len:要发送的数据长度
flag:默认0---表示阻塞发送
peeraddr:对端地址信息
addrlen:地址结构长度
返回值:成功返回实际发送的数据长度,失败返回-1(当发送缓冲区剩余空间大小小于要发送数据长度时,会等待或报错,即不允许出现只传输一部分数据的情况)
(5)关闭套接字
int close(int fd)
udp服务端代码实现:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h> //字节序转换接口头文件
#include<netinet/in.h> //地址结构头文件,协议类型头文件
#include<sys/socket.h> //套接字接口头文件
int main()
{
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //获取套接字文件描述符,套接字类型为数据报,协议类型为udp
if(sockfd < 0)
{
perror("socket error");
return -1;
}
//2.为套接字绑定地址信息
struct sockaddr_in addr; //定义IPv4地址结构
addr.sin_family = AF_INET; //设置IPv4地址域类型
addr.sin_port = htons(9000); //设置地址端口,并将主机字节序转换为网络字节序
addr.sin_addr.s_addr = inet_addr("192.168.191.130"); //设置IP地址,将点分十进制IP地址转换为网络字节序
int len = sizeof(addr); //计算地址信息长度
int ret = bind(sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return -1;
}
//3.接收数据
while(1)
{
char buf[1024] = {0}; //创建缓冲区用来存放数据
struct sockaddr_in paddr; //获取本条数据的源端地址信息
socklen_t len = sizeof(struct sockaddr_in); //指定要接收多长的地址结构,并且返回实际接收的地址长度
ret = recvfrom(sockfd, buf, 1023, 0, (struct sockaddr*)&paddr, &len);
if(ret < 0)
{
perror("recvfrom error");
return -1;
}
uint16_t cport = ntohs(paddr.sin_port); //获取传入数据客户端的端口,并将网络字节序转换为主机字节序
char* cip = inet_ntoa(paddr.sin_addr); //获取客户端的IP地址并转换为点分十进制
printf("client-[%d:%s] say: %s\n", cport, cip, buf);
//4.回复数据
memset(buf, 0x00, 1024); //清空buf将值全部置为0
printf("server say:");
fflush(stdout); //刷新输出缓冲区
fgets(buf, 1023, stdin); //从标准输入获取一行数据
ret = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&paddr, len);
if(ret < 0)
{
perror("sendto error");
return -1;
}
}
//5.关闭套接字
close(sockfd);
return 0;
}
封装实现一个udpsocket类:
//封装实现一个udpsocket类
//通过实例化的对象调用对应的成员接口,可以实现udp客户端或服务端的搭建
#include<cstdio>
#include<iostream>
#include<string>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
class UdpSocket{
private:
int _sockfd; //创建私有对象---套接字描述符
public:
UdpSocket():_sockfd(-1){} //构造函数
bool Socket() //创建套接字
{
_sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
bool Bind(std::string &ip, uint16_t port) //绑定地址信息
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str()); //c_str()函数返回一个指向数组的指针,该数组包含一个以null结尾>的字符序列(即C字符串),表示string对象的当前值。
socklen_t len = sizeof(struct sockaddr_in);
int ret;
ret = bind(_sockfd, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Send(std::string &data, const std::string &ip, const int port) //发送数据
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sockfd, data.c_str(), data.size(), 0, (struct sockaddr*)&addr, len);
if(ret < 0)
{
perror("sendto error");
return false;
}
return true;
}
bool Recv(std::string* buf, std::string* ip = NULL, int* port = NULL) //接收数据,由于客户端接收数据时不需要获>取服务端的地址信息,所以将地址信息默认初始化为空
{
struct sockaddr_in addr;
socklen_t len = sizeof(struct sockaddr_in);
char tmp[4096] = {0};
int ret = recvfrom(_sockfd, tmp, 4096, 0, (sockaddr*)&addr, &len);
if(ret < 0)
{
perror("recvfrom error");
return false;
}
buf->assign(tmp, ret); //assign函数是为字符串指定一个新值,替换其当前内容。自带申请空间拷贝数据,即将tmp里ret长度的数据赋值给buf
if(ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if(port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
bool Close()
{
if(_sockfd != -1)
{
close(_sockfd);
}
return true;
}
};
udp客户端的实现:
include"udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main()
{
UdpSocket sock;
//1.创建套接字
CHECK_RET(sock.Socket());
//2.绑定地址信息(不推荐)
while(1)
{
//3.发送数据
std::cout << "client say: ";
std::string buf;
std::cin >> buf;
CHECK_RET(sock.Send(buf, "192.168.191.130", 9000));
//4.接收数据
buf.clear();
CHECK_RET(sock.Recv(&buf));
std::cout << "server say: " << buf << "\n";
}
//5.关闭套接字
sock.Close();
return 0;
}
tcp通信程序的编写:
server通信流程:
(1)创建套接字
(2)为套接字绑定地址信息
(3)开始监听(告诉系统可以开始处理客户端的连接请求了,系统会为每一个新客户端创建一个新的套接字)
(4)获取新建连接
(5)收发数据(使用的是新建的套接字)
(6)关闭套接字
client通信流程:
(1)创建套接字
(2)为套接字绑定地址信息(不推荐主动绑定)
(3)向服务端发起连接请求
(4)收发数据
(5)关闭套接字
接口认识:
(1)开始监听
int listen(int sockfd, int backlog)
sockfd:描述符
backlog:服务端能够在同一时间处理的最大连接数
(2)客户端发送连接请求
int connect(int sockfd, struct sockaddr* srvaddr, socklen_t len)
sockfd:描述符
srvaddr:服务端地址信息
len:地址长度
返回值:成功返回0;失败返回-1
(3)服务端获取新建连接
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen)
sockfd:监听套接字---服务端最早创建的套接字,只用于获取新建连接
cliaddr:新连接的客户端地址信息
addrlen:输入输出参数,指定地址信息长度,以及返回实际长度
返回值:新建连接的套接字描述符---往后与客户端的通信都通过这个描述符完成
(4)收发数据:tcp通信因为socket结构中包含完整五元组因此不需要指定地址
ssize_t send(int sockfd, void* data, int len, int flag)
sockfd:描述符
data:要发送的数据
len:要发送的数据长度
flag:0---阻塞发送
返回值:成功返回实际发送的数据长度;失败返回-1;连接断开会触发异常(当发送缓冲区剩余空间大小小于要发送数据长度时,会发送等于剩余空间大小的数据,即可以只传输与一部分的数据)
ssize_t recv(int sockfd, void* buf, int len, int flag)
sockfd:描述符
buf:空间地址
len:要接收的数据长度
flag:0---阻塞接收
返回值:成功返回实际接收的数据长度;出错返回-1;连接断开返回0
封装实现一个tcpsocket类:
#include<cstdio>
#include<iostream>
#include<unistd.h>
#include<string>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/socket.h>
#define CHECK_RET(q) if((q) == false) {return -1;}
#define LISTEN_BACKLOG 5 //定义服务端能够在同一时间处理的最大连接数的宏
class TcpSocket{
private:
int _sockfd; //tcp操作句柄
public:
TcpSocket():_sockfd(-1){} //构造函数
bool Socket() //创建套接字
{
_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //套接字类型选择字节流,通信协议选择tcp
if(_sockfd < 0)
{
perror("socket error");
return false;
}
return true;
}
bool Bind(const std::string &ip, const uint16_t port) //绑定地址信息
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(&ip[0]);
socklen_t len = sizeof(sockaddr_in);
int ret = bind(_sockfd, (sockaddr*)&addr, len);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
bool Listen(int backlog = LISTEN_BACKLOG) //服务端开始监听
{
int ret = listen(_sockfd, backlog);
if(ret < 0)
{
perror("listen error");
return false;
}
return true;
}
bool Connect(const std::string &ip, const int port) //客户端发送连接请求,需传入服务端的ip地址以及端口号
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(&ip[0]);
socklen_t len = sizeof(sockaddr_in);
int ret = connect(_sockfd, (sockaddr*)&addr, len);
if(ret < 0)
{
perror("connect error");
return false;
}
return true;
}
bool Accept(TcpSocket* sock, std::string* ip = NULL, uint16_t* port = NULL) //服务端获取新建连接
{
sockaddr_in addr;
socklen_t len = sizeof(sockaddr_in);
int newfd = accept(_sockfd, (sockaddr*)&addr, &len); //获取新建连接的套接字描述符
if(newfd < 0)
{
perror("accept error");
return false;
}
sock->_sockfd = newfd;
if(ip != NULL)
{
*ip = inet_ntoa(addr.sin_addr);
}
if(port != NULL)
{
*port = ntohs(addr.sin_port);
}
return true;
}
bool Recv(std::string* buf)
{
char tmp[4096] = {0};
int ret = recv(_sockfd, tmp, 4096, 0);
if(ret < 0)
{
perror("recv error");
return false;
}
else if(ret == 0) //连接断开
{
printf("peer shutdown");
return false;
}
buf->assign(tmp, ret); //将数据拷贝到buf里
return true;
}
bool Send(const std::string &data)
{
int total = 0; //表示当前发送的数据大小
while(total < data.size()) //若发送的数据大小小于数据总大小,则循环发送
{
int ret = send(_sockfd, &data[0] + total, data.size() - total, 0); //返回实际发送的数据长度
if(ret < 0)
{
perror("send error");
return false;
}
total += ret; //改变total的值
}
return true;
}
bool Close() //关闭套接字
{
if(_sockfd != -1)
{
close(_sockfd);
}
return true;
}
};
tcp客户端的实现:
#include "tcpsocket.hpp"
int main(int argc, char* argv[])
{
//通过参数传入要连接的服务端的地址信息
if(argc != 3)
{
printf("usage: ./tcp_cli srvip srvport\n");
return -1;
}
std::string srvip = argv[1]; //保存服务端的ip地址
uint16_t srvport = std::stoi(argv[2]); //保存服务端的端口
TcpSocket cli_sock; //实例化客户端对象
//1.创建套接字
CHECK_RET(cli_sock.Socket());
//2.绑定地址信息(不推荐)
//3.向服务端发起连接请求
CHECK_RET(cli_sock.Connect(srvip, srvport));
//4.收发数据
while(1)
{
std::string buf;
std::cout << "client say: ";
std::cin >> buf; //从标准输入获取数据放到buf中
CHECK_RET(cli_sock.Send(buf)); //向服务端发送数据
buf.clear(); //清空缓冲区
CHECK_RET(cli_sock.Recv(&buf)); //从服务端接收数据
std::cout << "server say: " << buf << std::endl;
}
//5.关闭套接字
CHECK_RET(cli_sock.Close());
return 0;
}
tcp服务端的实现:
#include "tcpsocket.hpp"
int main(int argc, char* argv[])
{
//通过程序运行参数指定服务端要绑定的地址
if(argc != 3)
{
printf("usage: ./tcp_srv 192.168.191.130 9000\n");
return -1;
}
std::string srvip = argv[1]; //获取服务端ip地址
uint16_t srvport = std::stoi(argv[2]); //获取服务端端口
TcpSocket lst_sock; //实例化服务端监听套接字对象
CHECK_RET(lst_sock.Socket()); //创建套接字
CHECK_RET(lst_sock.Bind(srvip, srvport)); //绑定地址信息
CHECK_RET(lst_sock.Listen()); //开始监听
while(1) //循环获取新建连接
{
TcpSocket clisock; //实例化创建的新建连接对象
std::string cliip; //保存客户端的ip地址
uint16_t cliport; //保存客户端的端口
bool ret = lst_sock.Accept(&clisock, &cliip, &cliport);
if(ret == false)
{
continue;
}
//收发数据使用获取的新建套件字进行通信
std::cout << "get new connect: " << cliip << "-" << cliport << "\n";
std::string buf;
ret = clisock.Recv(&buf); //接收数据
if(ret == false)
{
clisock.Close();
continue;
}
std::cout << "client say: " << buf << std::endl;
buf.clear(); //清空缓冲区
std::cout << "server say: ";
std::cin >> buf; //向缓冲区写入数据
ret = clisock.Send(buf); //发送数据
if(ret == false)
{
clisock.Close();
}
}
lst_sock.Close(); //关闭套接字
return 0;
}
注意:程序在服务端与一个客户端进行一次通信后,此时客户端再次向服务端发送数据会出现问题,服务端无法接收客户端发送的数据,客户端也无法正常发送数据。原因是程序卡在了服务端获取新建连接accept这个接口处,因此下面的收发数据都无法完成。
由于accept、recv、send都是阻塞接口,任意一个接口的调用,都有可能会导致服务端流程阻塞
本质原因:当前的服务端,因为不知道什么时候有新连接到来,什么时候哪个客户端有数据到来,因此流程只能固定的去调用接口,但是这种调用方式有可能会造成阻塞
解决方案:多执行流并发处理,为每一个客户端都创建一个执行流负责与这个客户端进行通信
好处:主线程卡在获取新建连接这里,不会影响客户端的通信;某个客户端的通信阻塞,也不会影响主线程以及其他线程
在主线程中,获取新建连接,一旦获取到了则创建一个执行流,通过这个新建连接与客户端进行通信
多线程:普通线程与主线程数据共享,指定入口函数执行。主线程不能随意释放套接字,因为资源共享,一旦释放其他线程无法使用
通过创建多线程完成对tcp服务端的实现:
#include "tcpsocket.hpp"
//线程入口函数,专门负责与客户端的通信
void* thr_entry(void* arg)
{
TcpSocket *clisock = (TcpSocket*)arg;
while(1)
{
//收发数据使用获取的新建套件字进行通信
std::string buf;
bool ret = clisock->Recv(&buf); //接收数据
if(ret == false)
{
clisock->Close();
delete clisock;
return NULL;
}
std::cout << "client say: " << buf << std::endl;
buf.clear(); //清空缓冲区
std::cout << "server say: ";
std::cin >> buf; //向缓冲区写入数据
ret = clisock->Send(buf); //发送数据
if(ret == false)
{
clisock->Close();
delete clisock;
return NULL;
}
}
clisock->Close();
delete clisock;
return NULL;
}
int main(int argc, char* argv[])
{
//通过程序运行参数指定服务端要绑定的地址
if(argc != 3)
{
printf("usage: ./tcp_srv 192.168.191.130 9000\n");
return -1;
}
std::string srvip = argv[1]; //获取服务端ip地址
uint16_t srvport = std::stoi(argv[2]); //获取服务端端口
TcpSocket lst_sock; //实例化服务端监听套接字对象
CHECK_RET(lst_sock.Socket()); //创建套接字
CHECK_RET(lst_sock.Bind(srvip, srvport)); //绑定地址信息
CHECK_RET(lst_sock.Listen()); //开始监听
while(1) //循环获取新建连接
{
TcpSocket *clisock = new TcpSocket(); //实例化创建的新建连接对象
std::string cliip; //保存客户端的ip地址
uint16_t cliport; //保存客户端的端口
bool ret = lst_sock.Accept(clisock, &cliip, &cliport);
if(ret == false)
{
continue;
}
std::cout << "get new connect: " << cliip << "-" << cliport << "\n";
//创建线程专门负责与指定客户端的通信
pthread_t tid;
pthread_create(&tid, NULL, thr_entry, (void*)clisock);
pthread_detach(tid); //线程分离,线程退出会自动释放资源,不需要被等待
}
lst_sock.Close(); //关闭套接字
return 0;
}
子进程:子进程复制了父进程,但是数据独有。注意僵尸进程的处理,注意父子进程数据各自独有,父进程用不到新建套接字因此创建子进程之后直接释放掉,否则会造成资源泄露
通过创建子进程完成对tcp服务端的实现:
#include "tcpsocket.hpp"
#include<signal.h>
int main(int argc, char* argv[])
{
//通过程序运行参数指定服务端要绑定的地址
if(argc != 3)
{
printf("usage: ./tcp_srv 192.168.191.130 9000\n");
return -1;
}
signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号,即子进程退出直接释放资源,不会成为僵尸进程
std::string srvip = argv[1]; //获取服务端ip地址
uint16_t srvport = std::stoi(argv[2]); //获取服务端端口
TcpSocket lst_sock; //实例化服务端监听套接字对象
CHECK_RET(lst_sock.Socket()); //创建套接字
CHECK_RET(lst_sock.Bind(srvip, srvport)); //绑定地址信息
CHECK_RET(lst_sock.Listen()); //开始监听
while(1) //循环获取新建连接
{
TcpSocket clisock; //实例化创建的新建连接对象
std::string cliip; //保存客户端的ip地址
uint16_t cliport; //保存客户端的端口
bool ret = lst_sock.Accept(&clisock, &cliip, &cliport);
if(ret == false)
{
continue;
}
//收发数据使用获取的新建套件字进行通信
std::cout << "get new connect: " << cliip << "-" << cliport << "\n";
pid_t pid = fork(); //创建子进程
if(pid < 0)
{
clisock.Close();
continue;
}
else if(pid == 0) //子进程完成与客户端的通信
{
while(1)
{
std::string buf;
ret = clisock.Recv(&buf); //接收数据
if(ret == false)
{
clisock.Close();
exit(0);
}
std::cout << "client say: " << buf << std::endl;
buf.clear(); //清空缓冲区
std::cout << "server say: ";
std::cin >> buf; //向缓冲区写入数据
ret = clisock.Send(buf); //发送数据
if(ret == false)
{
clisock.Close();
exit(0);
}
}
clisock.Close(); //释放的是子进程的clisock
exit(0);
}
//父子进程数据独有,父进程关闭不会对子进程造成影响
clisock.Close(); //释放的是父进程中的clisock
}
lst_sock.Close(); //关闭套接字
return 0;
}