首先祝福课程设计负责老师祖坟冒火花,四天时间让人写一个软件出来也是有点东西的:)
1.IP地址
127.0.0.1 是本机地址。2.端口
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。3.协议
协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。4.数据传输方式
计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM 和 SOCK_DGRAM。
1) SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
2) SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
本次做的是视频聊天可以使用SOCK_DGRAM
tip:
socket() 函数用来创建套接字,确定套接字的各种属性,然后服务器端要用 bind() 函数将套接字与特定的IP地址和端口绑定起来,只有这样,流经该IP地址和端口的数据才能交给套接字处理;而客户端要用 connect() 函数建立连接。
http://c.biancheng.net/cpp/html/3033.html
直观性的体会SOCK_STREAM和SOCK_DGRAM之间的区别,sever端的accpet是阻塞还是非阻塞的,下面的代码如果换成DGRAM的话就无法运行,因为sever在accpet处不会阻塞等待
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
using namespace std;
int main(int argc, char const *argv[])
{
//创建套接字
int serv_sock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//绑定IP,端口
struct sockaddr_in serv_addr;
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET; //使用IPV4地址
serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
serv_addr.sin_port = htons(1234);
bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); //将套接字和端口与IP地址绑定
listen(serv_sock,20); //进入监听
//接收客户端请求
struct sockaddr_in client_addr;
socklen_t client_addr_size = sizeof(client_addr);
int client_sock = accept(serv_sock,(struct sockaddr*)&serv_addr,&client_addr_size);
//发送数据
char str[] = "hello world";
write(client_sock,str,sizeof(str));
//关闭套接字
close(serv_sock);
close(client_sock);
return 0;
}
netstat -tanlp
linux下的网络工具,用于查看各个ip地址和端口的状态
加个while循环之后就能一直发送
总结一下整个过程:
首先通过socket()函数创建套接字,需要选择地址方式AF_INET(IPV4),通讯方式(STREAM,DGRAM),协议(TCP,UDP)
然后通过sock_addr_in结构体创建地址和端口用的结构体
sever端需要通过bind将套接字和地址&端口绑定,client端用connect进行绑定
server端之后需要通过listen将端口列入监听
http://c.biancheng.net/cpp/html/3036.html
当sever处于listen状态时,可以通过accpet来接收客户端发来的请求
双方的数据发送和接收采用write和read来进行,也可以用send和recv
关于socket双方通讯的问题,目前我觉得最可靠的方式还是建立两个线程分别用于收发是最靠谱的
https://blog.csdn.net/chenglibin1988/article/details/8812506
成功实现双线程的点对点通信,现在开始做视频聊天的部分,视频的采集和播放可以使用opencv,嗯我不想折腾了,就直接opencv,音频采集使用alsa,gui也可以直接用opencv的,这样省了很多的功夫,比较开心
udp传输的时候注意一下,socket初始化的方法和tcp的差的很远,我没有新弄api,只是用原来的api加了功能选项
另外udp传输使用的api和tcp也有差别,需要使用sendto和recvfrom
使用opencv采集的mat直接使用udp进行传送的时候,发现一个主要的问题是一个mat太大了,必须缩小到1/16才能够进行发送,这样导致图像清晰度原地猝死,所以解决方案是建立一个fifo,将一个图像的信息分割成16次进行发送,接收方收到16次后再进行拼接
对于udp来说实际上就没有什么server和client的分别了,也就是只需要一个main,接着将sender和reciever分成两个线程,一个负责收一个负责发。
使用opencv来完成采集,使用videocapture类打开摄像头,采集图像放入Mat中,将Mat.data进行发送即可,发送前记得resize到原来图像的1/16,不然会超过udp一次能够发送的大小,所以清晰度会有比较大的损失,还有一个减小损失清晰度的方法是将一个图像分割成16份,一份一份的进行发送,接收方收到之后再一份一份拼起来,但是这种方法带来的延时会比较严重,所以最终选择了损失清晰度的方法。
发送方send线程
void videosendsocket(int *sock,struct sockaddr_in* recvaddr)
{
usleep(2000000);
int sendsock = *sock;
cv::VideoCapture cam;
cam.open(0);
cv::Mat frame;
cam >> frame;
socklen_t nLen = sizeof(*recvaddr);
cv::resize(frame,frame,cv::Size(),0.25,0.25);
int64_t framesize = frame.dataend - frame.datastart;
while (cam.isOpened())
{
cam >> frame;
if(frame.empty())
cout << "frame get error" << endl;
cv::resize(frame,frame,cv::Size(),0.25,0.25);
int status = sendto(sendsock,(char*)frame.data,framesize,0,(struct sockaddr *)recvaddr,nLen);
if (status == -1)
{
perror("sendto");
break;
}
if(cv::waitKey(30) == 'q')
{
cam.release();
break;
}
}
}
接收方recieve线程
void videorecvsocket(int *sock)
{
int recvsock = *sock;
cv::Mat frame;
cv::Mat output_frame;
cv::VideoCapture cam;
cam.open(0);
cam >> frame;
cv::resize(frame,frame,cv::Size(),0.25,0.25);
long int framesize = 57600;
cam.release();
struct sockaddr_in clint_addr;
socklen_t nLen = sizeof(clint_addr);
while (recvfrom(recvsock,(char *)frame.data,framesize,0,(struct sockaddr*)&clint_addr,&nLen) == -1)
{
usleep(1);
}
cout << "connected success" << endl;
while (1)
{
int recv_stat = recvfrom(recvsock,(char *)frame.data,framesize,0,(struct sockaddr*)&clint_addr,&nLen);
if(recv_stat == -1)
{
perror("frame recvfrom");
break;
}
if(frame.empty())
cout << "frame get error" << endl;
cv::resize(frame,output_frame,cv::Size(),2,2);
cv::imshow("server_recv",output_frame);
if(cv::waitKey(30) == 'q')
{
break;
}
}
}
将video的接收和发送相关的操作全部封装到了satori_video类下videorecvsocket和videosendsocket都为类下的成员函数,在main中通过thread将他们注册为发送线程和接收线程
成员函数作为线程入口的例子
thread* videorecvthread = new thread(&satori_video::videorecvsocket,video,&socket.udp_sock);
thread* videosendthread = new thread(&satori_video::videosendsocket,video,&socket.udp_sock,&socket.clint_addr);
音频的采集和收发采用alsa
https://blog.csdn.net/oyoung_2012/article/details/80524559
还顺手学了一下cmake里面的find_package的用法......唉我真是佛了,这个东西居然还能这么用,是我太菜了,之前都不知道有这个高级玩意
https://blog.csdn.net/u011092188/article/details/61425924
alsa提供了声卡的操作函数,通过API设置完毕采集的参数之后就可以开始采集,将采集到的声音存入一帧,然后通过udp协议发送。接收方同样先对声卡进行设置,收到一帧数据之后,放入声卡进行播放即可
接收方/发送方的声卡初始化
void satori_audio::init_pcm_capture(snd_pcm_t **handle,snd_pcm_uframes_t *frame)
{
snd_pcm_hw_params_t *params = NULL;
snd_pcm_open(handle,"default",SND_PCM_STREAM_PLAYBACK,0);
snd_pcm_hw_params_alloca(¶ms);
snd_pcm_hw_params_any(*handle,params);
snd_pcm_hw_params_set_access(*handle,params,SND_PCM_ACCESS_MMAP_INTERLEAVED);
snd_pcm_hw_params_set_format(*handle,params,SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(*handle,params,2);
samplerate = 44100;
snd_pcm_hw_params_set_rate_near(*handle,params,&samplerate,0);
snd_pcm_hw_params(*handle,params);
snd_pcm_hw_params_get_period_size(params,frame,0);
}
void satori_audio::init_pcm_playback(snd_pcm_t **handle,snd_pcm_uframes_t *frame)
{
snd_pcm_hw_params_t *params = NULL;
snd_pcm_open(handle,"default",SND_PCM_STREAM_PLAYBACK,0);
snd_pcm_hw_params_alloca(¶ms);
snd_pcm_hw_params_set_access(*handle,params,SND_PCM_ACCESS_RW_INTERLEAVED);
snd_pcm_hw_params_set_format(*handle,params,SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_channels(*handle,params,2);
samplerate = 44100;
snd_pcm_hw_params_set_rate_near(*handle,params,&samplerate,0);
snd_pcm_hw_params(*handle,params);
snd_pcm_hw_params_get_period_size(params,frame,0);
}
通过UDP协议进行发送和接收
void satori_audio::audiosendsocket(int *sock,struct sockaddr_in* recvaddr)
{
struct snd_pcm sp_capture;
int sendsock = *sock;
socklen_t nLen = sizeof(*recvaddr);
sp_capture.handle = NULL;
sp_capture.frame = 0;
init_pcm_capture(&sp_capture.handle,&sp_capture.frame);
int size = sp_capture.frame * 4;
char *buffer = (char *)malloc(size);
while (1)
{
snd_pcm_readi(sp_capture.handle,buffer,sp_capture.frame);
int status = sendto(sendsock,(char*)buffer,size,0,(struct sockaddr *)recvaddr,nLen);
if (status == -1)
{
perror("sendto");
break;
}
}
snd_pcm_drain(sp_capture.handle);
snd_pcm_close(sp_capture.handle);
free(buffer);
}
void satori_audio::audiorecvsocket(int *sock)
{
int recvsock = *sock;
struct sockaddr_in clint_addr;
socklen_t nLen = sizeof(clint_addr);
struct snd_pcm sp_playback;
sp_playback.handle = NULL;
sp_playback.frame = 0;
init_pcm_playback(&sp_playback.handle,&sp_playback.frame);
int size = sp_playback.frame * 4;
char *buffer = (char *)malloc(size);
while (1)
{
int status = recvfrom(recvsock,(char*)buffer,size,0,(struct sockaddr *)&clint_addr,&nLen);
if (status == -1)
{
perror("recvfrom");
break;
}
snd_pcm_writei(sp_playback.handle,buffer,sp_playback.frame);
}
snd_pcm_drain(sp_playback.handle);
snd_pcm_close(sp_playback.handle);
free(buffer);
}
最后通过qt实现界面的gui设计
qt同样可以通过控件的方式进行设计。每一个控件可以生成一个对应的信号槽,在信号槽中完成对应的控件功能。
通过push_button控件实现摄像头开关和麦克风开关的槽函数:
void MainWindow::on_open_video_clicked()
{
std::cout << "camera opened" << std::endl;
video.cam.open(0);
timer->start(20);
}
void MainWindow::on_close_video_clicked()
{
std::cout << "camera closed" << std::endl;
timer->stop();
video.cam.release();
}
void MainWindow::on_open_microphone_clicked()
{
std::cout << "microphone opened" << std::endl;
audio.init_pcm_capture(&audio.sp_capture.handle,&audio.sp_capture.frame);
audio.init_pcm_playback(&audio.sp_playback.handle,&audio.sp_playback.frame);
}
void MainWindow::on_close_microphone_clicked()
{
std::cout << "microphone closed" << std::endl;
snd_pcm_drain(audio.sp_capture.handle);
snd_pcm_drain(audio.sp_playback.handle);
}
另外qt还提供了Qtimer提供计时器功能,这个功能能够定时执行一些任务,比如摄像头的读取和显示,定时器初始化定时周期之后,计数到达溢出时间之后就会自动触发回调函数,这里将本地摄像头的图像输出到了gui界面上
void MainWindow::show_self_frame()
{
video.cam >> video.frame;
cv::resize(video.frame,video.frame,cv::Size(),0.25,0.25);
QImage image = QImage((const uchar*)video.frame.data,video.frame.cols,video.frame.rows,QImage::Format_RGB888).rgbSwapped();
ui->self_frame->setPixmap(QPixmap::fromImage(image));
}
加密部分采用RSA算法进行加密。首先通过RSA_init函数进行RSA的初始化,产生两个随机素数p,q,利用这对素数的乘积n和欧几里得拓展算法获取公钥PU(e,n)和私钥PR=(d,n),将私钥保存在本地,将公钥发送给通讯方
void rsa::RSA_Initialize(int *n,int *e,int *d)
{
int prime[5000];
int count_Prime = ProducePrimeNumber(prime);
srand((unsigned)time(NULL));
int ranNum1 = rand()%count_Prime;
int ranNum2 = rand()%count_Prime;
int p = prime[ranNum1], q = prime[ranNum2];
*n = p*q;
int On = (p-1)*(q-1);
for(int j = 3; j < On; j+=1331)
{
int gcd = Exgcd(j, On, *d);
if( gcd == 1 && *d > 0)
{
*e = j;
break;
}
}
}
通讯方收到公钥之后,采用公钥对要发送的明文信息进行加密,然后将密文发送给接收方
void rsa::RSA_Encrypt(char *Ciphertext,char *Plaintext,int n,int e)
{
cout<<"Public Key (e, n) : e = "<<e<<" n = "<<n<<'\n';
int i = 0;
for(i = 0; i < 100; i++)
Ciphertext[i] = Modular_Exonentiation(Plaintext[i], e, n);
cout<<"Use the public key (e, n) to encrypt:"<<'\n';
for(i = 0; i < 100; i++)
cout<<Ciphertext[i]<<" ";
cout<<'\n'<<'\n';
}
接收方收到密文之后,用私钥进行解密,就可以在本地获得明文
void rsa::RSA_Decrypt(char *Ciphertext,int d,int n)
{
int i = 0;
for(i = 0; i < 100; i++)
Ciphertext[i] = Modular_Exonentiation(Ciphertext[i], d, n);
cout<<"Use private key (d, n) to decrypt:"<<'\n';
for(i = 0; i < 100; i++)
cout<<Ciphertext[i]<<" ";
cout<<'\n'<<'\n';
}