一、预备知识
什么是套接字编程?
套接字编程就是网络通信程序的编写,现在网络中的各种通信都是客户与服务器之间的通信,用户与用户之间的通信,本质上就是用户与服务器之间通信,然后服务器再与另一个用户进行通信,从而实现用户和用户之间的通信,要实现这些,都需要依靠socket,以下将介绍如何进行套接字编程。
源端IP地址和目的端IP地址
在网络通信中,每一台主机都有自己的IP地址,A主机向B主机发送数据,B主机的IP地址被称为目的端IP地址,A主机的IP地址称为源端IP地址
源端口号和目的端口号
端口号:是传输层协议的内容
(1) 端口号本质是一个2字节的16位的整数
(2)端口号用来标识一个主机上的一个进程,告诉操作系统,当前这个数据要交给哪一个进程来处理
(3) IP地址 + 端口号能标识某以网络中的某一主机上的某一个进程
(4) 一个端口号只能被一个占用
/* 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定 */
源端IP地址的端口号被称为源端空号,目的端IP地址的端口号被称为目的端口号,他们描述了“数据是谁发的,要发给谁”
网络字节序
大端:数据的高字节保存在内存中的低地址处,低字节保存在高地址处
小端:数据的高字节保存在内存中的高地址处,低字节保存在低地址处
网络字节序为 大端模式
在网络通信中,需要不同的主机对网络字节序进行转换,如果该主机也是大端模式,则不需要进行转换,如果该主机是小端模式,则该主机收到其他主机发来的数据后,会将该数据由网络字节序转化为该主机自己的字节序
接口:
//16位数据的转化
uint16_t htons(uint16_t hostshort); //主机字节序转化为网络字节序
uint16_t ntohs(uint16_t netshort); //网络字节序转化为主机字节序
//32位数据的转化
uint32_t htonl(uint32_t hostlong); //主机字节序转化为网络字节序
uint32_t ntohl(uint32_t netlong); //网络字节序转化为主机字节序
注:16位数据只能用htons 或者 ntohs 进行转化,32位数据只能用 htonl 或 ntohl转化,不能反着用
地址转换函数
in_addr_in inet_addr(const char* ip); //将点分十进制的字符串IP地址转化为网路字节序的整数IP地址
//例子:192.168.2.2 ----> 0x0202a8c0
const char* inet_ntoa(struct in_addr addr); //将网络字节序整数IP地址转化为点分十进制的字符串IP地址
网络传输数据的五元组
源端IP地址
源端端口
目的端IP地址
目的端端口
协议
二、UDP通信程序
UDP协议
UDP是 User Datagram Protocol 的缩写,即用户数据报协议
特点:传输层协议,是一种无连接,不可靠,提供数据报传输的服务
应用场景:性能要求大于安全要求,比如视频,音频传输
通信流程
a.服务端通信流程
1. 创建套接字(本质上是在内核中创建一个socket结构体,用于关联网卡与当前通信进程)
2.为套接字绑定地址信息(将该主机的一个端口地址绑定到socket中,告诉操作系统,当接收到数据时候,将这个发送的地址信息与socket绑定的地址信息一样的数据,将给这个socket进行处理)
3.接收数据(从socket的接收缓冲区中取出数据,同时也可以知道这个数据是谁发的)
4.发送数据(将要发送的数据放入到socket的发送缓冲区中国,并告诉系统这个数据要发送给谁)
5.关闭套接字,释放资源
b.客户端通信流程
1.创建套接字
2.为套接字绑定地址信息(不建议),系统会自动绑定一个地址
因为客户端一旦绑定了地址信息,则发送数据的源端地址就是固定的,这时候这个端口只能被这个进 程占用,容易引发冲突
3.发送数据(将要发送的数据放到该socket的发送缓冲区中,并告诉该系统这个数据要发送给谁)
4.接收数据(从socket接收缓冲区中取出数据)
5.关闭套接字,释放资源

//接口:
//1. 创建套接字
int socket(int domain,int type,int protocol);
//domain: 地址域类型(ipv4通信,ipv6通信,域间通信...,不同的通信方式有不同的地址结构,主要是ipv4通信)
// AF_INET: IPV4地址域类型
//type: 套接字类型
// SOCK_STREAM: 流式套接字,提供字节流传输,默认是TCP协议
// SOCK_DRGAM: 数据报套接字,提供数据报传输,默认是UDP协议
//protocol: 协议类型
// IPPROTO_TCP,值为6
// IPPROTO_UDP,值为17
//返回值:成功返回一个套接字的描述符,失败返回-1
//2. 为套接字绑定地址信息
int bind(int sockfd,struct sockaddr* addr,socklen_t len);
//sockfd: 创建套接字返回的描述符
//addr: 要绑定的地址信息
//len: 要绑定的地址信息的长度
//3. 接收数据
ssize_t recvfrom(int sockfd,void* buf,size_t dlen,int flag,struct sockaddr*peer,socklen_t *len);
//sockfd: 创建套接字返回的描述符
//buf: 接收的数据放入buf
//dlen: 接收数据的长度
//flag: 默认为0,阻塞接收(接收缓冲区中没有数据则阻塞)
//peer: 接收数据的同时,将发送数据的地址保存到peer中,是一个输出参数
//len: 表示想要接收的地址信息长度,以及实际接收的地址信息长度,是一个输入输出参数
//成功返回实际接收的数据长度,失败返回-1
//4. 发送数据
ssize_t sento(int sockfd,void* buf,size_t dlen,int flag,strcut sockaddr*peer,socklen_t len);
//sockfd: 创建套接字返回的描述符
//buf: 要发送的数据
//dlen: 要发送数据的长度
//flag: 默认为0,阻塞接收(发送缓冲区数据满了就阻塞)
//peer: 向peer这个对端地址发送数据
//len: 表示对端地址信息长度
//成功返回实际发送的数据长度,失败返回-1
//5. 关闭套接字
int close(int sockfd);
//IPV4 地址结构
struct sockaddr_in
{
sa_family sin_family; //2字节地址域类型
in_port_t sin_port; //2字节的端口
struct in_addr sin_addr //4字节的IP地址
char sin_zero[sizeof(stuct sockaddr)-8]; //8字节补位
}
typedef uint32_t in_addr_t
struct in_addr
{
in_addr_t s_addr;
}

简单DUP通信程序
服务端

客户端

运行结果:
服务端

客户端

三、TCP通信程序
TCP协议
TCP是 Transmission Control Protocol 的缩写,即传输控制协议。
特点:面向连接,可靠传输,提供字节流传输服务
适用场景:传输安全性要求大于实时性要求的场景,例如文件传输
通信流程
服务端:
1.创建套接字,即在内核中创建一个socket 结构体
2.为套接字绑定地址信息(与UDP通信相同)
3.开始监听
将套接字的状态置为listen状态,只有在listen状态下,才能处理客户端的请求
4.获取新连接
当客户端向服务端发送连接请求成功后,这时候客户端向服务端发送数据,服务端会为这个客户端重新创建一个新的套接字,专门与之通信,之前第一步创建的套接字仅仅只是为了客户端和服务端之间建立连接,并不实际进行通信,也可以称第一步创建出来的这个套接字为监听套接字,获取新连接就是从监听套接字对应的新建连接队列中,取出一个套接字结构,获取到这个套接字的操作句柄与客户端进行通信
5.收发数据
这一步使用的是新创建的套接字的描述符来与对应的客户端进行通信,不需要指定客户端先发送数据
6.关闭套接字
客户端:
1.创建套接字
2.为套接字绑定地址信息
3.向服务端发送连接建立请求(连接一旦建立成功,服务端就会重新创建一个socket,在这个socket中,就会描述一个完整的无元组信息,用其与客户端进行通信)
4. 收发数据
5.关闭套接字

//接口
//1. 创建套接字
int socket(int domain,int type,int portocol); //与UDP通信相同
//2. 绑定地址信息
int bind(int sockfd,struct sockaddr* addr,socklen_t len);
//3. 开始监听(只服务端使用)
int listen(int sockfd,int backlog);
//sockfd: 创建套接字返回的套接字描述符
//backlog: 同一时刻最大并发连接数(后面详细讲解)
//4.请求连接(只客户端使用)
int connect(int sockfd,struct sockaddr* addr,socklen_t len);
//sockfd: 套接字描述符
//addr: 服务端地址信息
//len: 服务端地址信息长度
//5.获取新连接
int accept(int sockfd,struct sockaddr* addr,socklen_t* addrlen);
//功能描述: 从内核sockfd中指定套接字对应的已完成队列中,取出一个socket,并返回这个socket的描述符
//addr: 输出参数,accept内部将新建的socket结构体中客户端的地址信息存放在addr中
//addrlen: 输入输出参数,用于指定想要获取的地址长度,以及实际返回的地址长度
//返回值: 成功返回新建套接字描述符,失败返回-1
//6.收发数据
ssize_t send(int sockfd,void* data,size_t len ,int flag);
//sockfd: 套接字描述符
//data: 要发送的数据
//len: 要发送数据的长度
//flag: 默认0,阻塞发送
//返回值: 成功返回实际发送的数据长度,失败返回-1
ssize_t recv(int sockfd,void* buf,size_t len,int flag);
//buf: 接收的数据放入buf中
//len: 实际接收数据的长度
//返回值:成功返回实际接收数据的长度,失败返回-1,返回0则表示客户端与服务端连接断开,将无法进行通信
//7.关闭套接字
int close(int sockfd);
对于上述 int listen(int sockfd,int backlog); 中backflog这个参数表示的是服务端同一时刻的最大并发连接数,即限制同一时间只能有多少个客户端的连接请求被处理

在TCP通信中,涉及到对多个socket进行操作,即有多少个客户端发送连接请求成功后,就会新建多少个socket,而且每一个socket都要进行accept,recv,send操作,但是这三步操作都是阻塞操作,对于accept来说,如果没有新连接到来,就会阻塞;对于recv来说,如果客户端没有发送数据就会阻塞,因此一旦服务端获取了新连接,但客户端一直不发送数据,服务端就会一直卡在recv接收数据这块,就无法进行下一次获取新连接操作(即服务端无法与另一个客户端进行通信);其次,当客户端和服务端依次通信成功后,程序运行到accept这里,如果这时候没有新的连接到来,即没有其他客户端请求通信,程序就会卡在accept这里,直到有新的客户端的请求到来,才能继续通信,这时候原来客户端再次发送数据给服务端,服务端也不会进行处理。

解决方法
在服务端中,循环获取新连接,创建一个子进程或者线程,让其进行数据的接收和发送,每个执行流只负责一件事情,就算一个执行流因为某个事件阻塞,也不会影响其他执行流的运行
简单TCP通信程序
//tcp.hpp 封装一个TcpSocket结构体
#include<iostream>
#include<string>
#include<cstdio>
#include<unistd.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
class TcpSocket
{
private:
int _sockfd;
public:
TcpSocket()
:_sockfd(-1)
{}
~TcpSocket(){}
//创建套接字
bool Socket()
{
_sockfd = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(_sockfd<0)
{
perror("socket error");
return false;
}
return true;
}
//为套接字绑定地址信息
bool Bind(const 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());
socklen_t addrlen = sizeof(struct sockaddr_in);
int ret = bind(_sockfd,(struct sockaddr*)&addr,addrlen);
if(ret < 0)
{
perror("bind error");
return false;
}
return true;
}
//开始监听
bool Listen(int backlog = 10)
{
int ret = listen(_sockfd,backlog);
if(ret < 0)
{
perror("listen error");
return false;
}
return true;
}
//获取新连接
bool Accept(TcpSocket* sock,std::string* cli_ip=NULL,uint16_t* cli_port=NULL)
{
struct sockaddr_in peer;
socklen_t len = sizeof(struct sockaddr_in);
int newfd = accept(_sockfd,(struct sockaddr*)&peer,&len);
if(newfd < 0)
{
perror("accept error");
return false;
}
sock->_sockfd = newfd;
if(cli_ip!=NULL)
{
*cli_ip = inet_ntoa(peer.sin_addr);
}
if(cli_port!=NULL)
{
*cli_port = ntohs(peer.sin_port);
}
return true;
}
//接收数据
bool Recv(std::string* buf)
{
char temp[1024] = {0};
ssize_t ret = recv(_sockfd,temp,1023,0);
if(ret < 0)
{
perror("recv error");
return false;
}
else if(ret == 0)
{
printf("connect borken");
}
buf->assign(temp,ret);
return true;
}
//发送数据
bool Send(const std::string& buf)
{
ssize_t ret = send(_sockfd,buf.c_str(),buf.size(),0);
if(ret < 0)
{
perror("send error");
return false;
}
return true;
}
//客户端发送连接请求
bool Connect(const std::string& ip,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 = connect(_sockfd,(struct sockaddr*)&addr,len);
if(ret < 0)
{
perror("connect error");
return false;
}
return true;
}
//关闭套接字
bool Close()
{
if(_sockfd!=-1)
{
close(_sockfd);
_sockfd = -1;
}
return true;
}
};
多进程TCP通信版本
//服务端
#include"tcp.hpp"
#include<cassert>
#include<signal.h>
void create_work(TcpSocket new_sock)
{
//创建一个子进程,让其完成客户端和服务端之间的数据收发
pid_t ret =fork();
if(ret < 0)
{
perror("fork error");
new_sock.Close();
return;
}
if(ret>0)
{
new_sock.Close();
return;
}
while(1)
{
//5. 收发数据
std::string buf;
bool ret = new_sock.Recv(&buf);
if(ret == false)
{
new_sock.Close();
break;
}
std::cout<<"client say: "<<buf<<std::endl;
buf.clear();
std::cout<<"server say: ";
fflush(stdout);
std::cin>>buf;
ret = new_sock.Send(buf);
if(ret == false)
{
new_sock.Close();
break;
}
}
exit(-1);
}
//服务端
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("./ tcp_srv 192.168.2.2 9000\n");
return -1;
}
//提取ip地址和端口
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
signal(SIGCHLD,SIG_IGN);
//1. 创建套接字
TcpSocket sock;
assert(sock.Socket());
//2. 绑定地址信息
assert(sock.Bind(ip,port));
//3. 监听
assert(sock.Listen());
while(1)
{
//4. 获取新连接
TcpSocket new_sock;
std::string cli_ip;
uint16_t cli_port;
bool ret = sock.Accept(&new_sock,&cli_ip,&cli_port);
if(ret==false)
{
continue;
}
std::cout<<"new clinet: "<<cli_ip<<":"<<cli_port<<std::endl;
create_work(new_sock);
}
//6. 关闭套接字
sock.Close();
return 0;
}
//客户端
#include<cassert>
#include"tcp.hpp"
//客户端
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("./ tcp_cli 192.168.2.2 9000\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
//1. 创建套接字
TcpSocket cli_sock;
assert(cli_sock.Socket()!=false);
//2. 绑定地址信息(不推荐)
//3. 发送连接请求
assert(cli_sock.Connect(ip,port));
//4. 循环发送接收数据
while(1)
{
// 发送数据
std::string data;
std::cout<<"clinet say: ";
fflush(stdout);
std::cin>>data;
assert(cli_sock.Send(data));
// 接收数据
data.clear();
assert(cli_sock.Recv(&data));
std::cout<<"server say: "<<data<<std::endl;
}
//5. 关闭套接字
cli_sock.Close();
return 0;
}
多线程TCP通信版本
//服务端
#include"tcp.hpp"
#include<cassert>
#include<pthread.h>
void* thread_entry(void* arg)
{
TcpSocket new_sock;
long fd = (long)arg;
new_sock.set(fd);
while(1)
{
std::string buf;
bool ret = new_sock.Recv(&buf);
if(ret == false)
{
new_sock.Close();
break;
}
std::cout<<"client say: "<<buf<<std::endl;
buf.clear();
std::cout<<"server say: ";
fflush(stdout);
std::cin>>buf;
ret = new_sock.Send(buf);
if(ret == false)
{
new_sock.Close();
break;
}
}
}
void create_work(TcpSocket sock)
{
pthread_t tid;
//创建线程,让线程进行客户端与服务端之间的收发数据工作
long Fd = sock.fd();
int ret = pthread_create(&tid,NULL,thread_entry,(void*)Fd);
if(ret!=0)
{
printf("pthread_create error\n");
sock.Close();
return;
}
pthread_detach(tid); //分离线程,线程退出后自动释放资源
}
//服务端
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("./ tcp_srv 192.168.2.2 9000\n");
return -1;
}
//提取ip地址和端口
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
//1. 创建套接字
TcpSocket sock;
assert(sock.Socket());
//2. 绑定地址信息
assert(sock.Bind(ip,port));
//3. 监听
assert(sock.Listen());
while(1)
{
//4. 获取新连接
TcpSocket new_sock;
std::string cli_ip;
uint16_t cli_port;
bool ret = sock.Accept(&new_sock,&cli_ip,&cli_port);
if(ret==false)
{
continue;
}
std::cout<<"new clinet: "<<cli_ip<<":"<<cli_port<<std::endl;
//创建线程
create_work(new_sock);
}
//6. 关闭套接字
sock.Close();
return -1;
}
//客户端
#include<cassert>
#include"tcp.hpp"
//客户端
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("./ tcp_cli 192.168.2.2 9000\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = std::stoi(argv[2]);
//1. 创建套接字
TcpSocket cli_sock;
assert(cli_sock.Socket()!=false);
//2. 绑定地址信息(不推荐)
//3. 发送连接请求
assert(cli_sock.Connect(ip,port));
//4. 循环发送接收数据
while(1)
{
// 发送数据
std::string data;
std::cout<<"clinet say: ";
fflush(stdout);
std::cin>>data;
assert(cli_sock.Send(data));
// 接收数据
data.clear();
assert(cli_sock.Recv(&data));
std::cout<<"server say: "<<data<<std::endl;
}
//5. 关闭套接字
cli_sock.Close();
return 0;
}
运行结果:
服务端

客户端1:

客户端2:
