3. 完成TCP网络编程
三次挥手的阶段:
四次挥手的阶段:
3.1 完善
- 从终端输入 ip 地址和端口号,使用 int argc 和 char* argv[]
- 需要多条信息的发送和接收,使用无限循环
- 为了不受输入堵塞的影响,让客户端和服务器之间多发消息,收发信息用各自的进程
客户端开一个进程,让父进程写子进程读
服务器也开一个进程,让父进程读子进程写
(或者用线程 thread,线程创建要在前面,并使用 detach 让系统回收资源) - 在一边提前关闭后,会出现另一边无限循环读取,为避免这种情况
对 read 的返回值判断,如果是0,表示对方已经提前关闭,直接退出循环
客户端:
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h> // AF_INET sockaddr_in
#include <unistd.h> // write() read() close()
#include <thread>
using namespace std;
int main(int argc,char* argv[]){
if(3 != argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2])); // atoi把字符串转为数字
int res = connect(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("connect error");
return 1;
}
// 创建子线程
thread t( [fd](){
for(;;){
char buffer[256] = {0};
int n = read(fd,buffer,256); // block
if(0 == n){
printf("server exit\n");
break;
}
cout << buffer << endl;
}
} );
t.detach();
// 主线程
string s;
while(cin >> s){ // block
write(fd,s.c_str(),s.size()+1);
}
close(fd);
}
服务器:
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
using namespace std;
int main(int argc,char* argv[]){
if(3 != argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2]));
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res){
perror("listen error");
return 1;
}
int connfd = accept(fd,NULL,NULL); // block,堵塞,等待客户端连接
if(-1 == connfd){
perror("accept error");
return 1;
}
// 创建子线程
thread t( [connfd](){
// 无限循环可多次写入和读取
for(;;){
char buffer[256] = {0};
int n = read(connfd,buffer,256); // block
if(0 == n){
printf("client exit\n");
break;
}
cout << buffer << endl;
}
} );
t.detach();
// 主线程
string s;
while(cin >> s){ // block
write(connfd,s.c_str(),s.size()+1);
}
close(connfd);
close(fd);
}
结果为:
3.2 属性
设置属性:
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen)
参数 | 含义 | eg.设置端口释放 |
---|---|---|
sockfd | 套接字描述符 | 监听描述符 fd |
level | 选项层次 | SOL_SOCKET 通用套接字选项 |
optname | 选项 | SO_REUSEADDR 让端口释放后立即就可以被再次使用,一个端口释放后会等待两分钟之后才能再被使用 |
optval | 选项值指针 | flag值设为1的指针 &flag |
optlen | optval缓冲区长度 | flag的长度 sizeof(flag) |
- 让端口释放后立即就可以被再次使用
- 在服务器显示客户端 IP 和端口号
服务器:
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
using namespace std;
int main(int argc,char* argv[]){
if(3 != argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
// 设置属性:端口释放
int flag = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2]));
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res){
perror("listen error");
return 1;
}
sockaddr_in remote_addr; // IPv4套接字地址结构
socklen_t len = sizeof(remote_addr);
int connfd = accept(fd,(sockaddr*)&remote_addr,&len); // block
cout << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
// 整型转换为字符串,网络序转主机序
if(-1 == connfd){
perror("accept error");
return 1;
}
thread t( [connfd](){
for(;;){
char buffer[256] = {0};
int n = read(connfd,buffer,256); // block
if(0 == n){
printf("client exit\n");
break;
}
cout << buffer << endl;
}
} );
t.detach();
// 主线程
string s;
while(cin >> s){ // block
write(connfd,s.c_str(),s.size()+1);
}
close(connfd);
close(fd);
}
连接后服务器端结果:
[root@foundation1 C++7.18]# g++ server2.cpp -o server2 -pthread
[root@foundation1 C++7.18]# ./server2 0.0.0.0 8081
127.0.0.1:56998
3.3 多个客户端的和单个服务器通信
每来一个客户端,服务器就创建一个线程
服务器:
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
#include <vector>
using namespace std;
int main(int argc,char* argv[]){
if(3 != argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
// 设置属性:端口释放
int flag = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2]));
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res){
perror("listen error");
return 1;
}
vector<int> fds; // 建立一个vector
// 服务器对每个客户端都发信息
thread writer( [&](){
string s;
while(cin >> s){ // block
for(auto connfd:fds)
write(connfd,s.c_str(),s.size()+1);
}
} );
writer.detach();
// 没来一个客户端,就需要建立一个connfd
for(;;){
sockaddr_in remote_addr;
socklen_t len = sizeof(remote_addr);
int connfd = accept(fd,(sockaddr*)&remote_addr,&len); // block
cout << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
if(-1 == connfd){
perror("accept error");
return 1;
}
fds.push_back(connfd); // 收所有客户端的connfd
thread t( [connfd](){
for(;;){
char buffer[256] = {0};
int n = read(connfd,buffer,256); // block
if(0 == n){
printf("client exit\n");
break;
}
cout << buffer << endl;
}
} );
t.detach();
}
for(auto connfd:fds) close(connfd);
close(fd);
}
结果为:
3.4 代码格式化工具astyle
- 下载
- 解压 tar -zxvf astyle安装包
- 进入编译目录 cd astyle目录/build/gcc
- 编译 make
- 安装 make install
- 测试 astyle -h 有说明内容,表示安装成功
使用 astyle 工具:
astyle server2.cpp
[root@foundation1 C++7.18]# astyle server2.cpp
Formatted /root/Desktop/C++基础/C++7.18/server2.cpp
内容帮助你格式化
还可以用别的格式化风格:
astyle --style=google server2.cpp
谷歌格式化风格
astyle --style=linux server2.cpp
linux格式化风格
astyle --style=gnu server2.cpp
gnu格式化风格
批量格式化:
astyle --style=google *.cpp
把当前所有cpp文件格式化
3.5 上传/下载文件
服务端:发送
直接传送文件函数 sendfile(传到哪儿, 文件, 偏移量, 文件长度);
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
int main(int argc,char** argv){
if(4!=argc){ // 终端输入文件名
printf("argument error\n");
printf("Usage:%s server_ip server_port work_path\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton("127.0.0.1",&addr.sin_addr);
addr.sin_port = htons(8080);
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res){
perror("listen error");
return 1;
}
int connfd = accept(fd,NULL,NULL);
if(-1 == connfd){
perror("accept error");
return 1;
}
char buffer[256] = {0};
read(connfd,buffer,256);
cout << buffer << endl;
string file = string(argv[3])+"/"+buffer;
printf("require %s\n",file.c_str());
int filefd = open(file.c_str(),O_RDONLY); // 打开文件,只读
if(-1 == filefd){
perror("open file error");
return 1;
}
struct stat s; // 结构体,st_size能够获取文件大小
fstat(filefd,&s);
sendfile(connfd,filefd,0,s.st_size); // 可以直接发文件
// write(connfd,s.c_str(),s.size()+1);
close(connfd);
close(fd);
}
客户端:下载
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
int main(int argc,char* argv[]){
if(3!=argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton("127.0.0.1",&addr.sin_addr);
addr.sin_port = htons(8080);
int res = connect(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("connect error");
return 1;
}
string s;
cin >> s;
write(fd,s.c_str(),s.size()+1);
string file = "./"+s; // 当前路径下加文件名
cout << file << endl;
int filefd = open(file.c_str(),O_CREAT|O_WRONLY,0666); // 创建一个文件
if(-1 == filefd){
perror("open file error");
return 1;
}
for(;;){
char buffer[256] = {0};
int n = read(fd,buffer,256);
write(filefd,buffer,n); // 读到的内容写到刚创建的文件里面
if(n < 256) break;
}
close(filefd);
close(fd);
}
上传/下载步驟:
- 服务端终端输入要下载文件的路径,等待客户端下载
- 客户端与服务端连通后,输入要下载的文件名
- 客户端就能把文件下载到当前文件夹了,下载后两端都关闭
也可以在上传端输入文件目录的路径,但在下载端得先创建该目录,再下载,下载的路径才能找到
为了解决这个问题:
substr
切割字符
find_last_of('/')
查找 ’ / ’ 最后一次出现的位置
即使下载端输入的是目录和文件,也只会把文件下载到当前文件
string s;
cin >> s;
write(fd,s.c_str(),s.size()+1);
int pos = s.find_last_of('/'); // 找到最后文件名
if(pos != string::npos){ // 如果找到了,进行切割
s = substr(pos);
}
string file = "./"+s; // 当前路径下加文件名
cout << file << endl;
int filefd = open(file.c_str(),O_CREAT|O_WRONLY,0666); // 创建一个文件
if(-1 == filefd){
perror("open file error");
return 1;
}
3.6 对应目录下载
先服务器发给客户端有哪些文件,客户端再发送要下载的文件
使用管道在服务端执行命令ll -R
(递归目录下的所有文件信息),把执行结果发送给客户端
管道读取 size_t fread ( 读到哪儿, 字节数, 大小, 从哪儿读)
服务端:上传
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstdio>
#include <thread>
using namespace std;
int main(int argc,char** argv){
if(4!=argc){ // 终端输入文件名
printf("argument error\n");
printf("Usage:%s server_ip server_port work_path\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton("127.0.0.1",&addr.sin_addr);
addr.sin_port = htons(8080);
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res){
perror("listen error");
return 1;
}
for(;;){ // 多次接受
int connfd = accept(fd,NULL,NULL);
if(-1 == connfd){
perror("accept error");
return 1;
}
// 发送管道执行结果
string cmd = string("ls -lR ")+argv[3]; // 获取命令信息,lR后面要加空格
FILE* pfile = popen(cmd.c_str(),"r");
if(NULL == pfile){
perror("popen error");
return 1;
}
for(;;){
char info[1024] = {0};
int n = fread(info,1,1024,pfile); // 读取管道执行结果
write(connfd,info,n); // 把结果写在连接中
if(n < 1024) break;
}
pclose(pfile);
thread t( [=](){ // 按照值的方式=捕获
for(;;){
// 发送文件
char buffer[256] = {0};
int n = read(connfd,buffer,256);
if(n <= 0) break;
cout << buffer << endl;
string file = string(argv[3])+"/"+buffer;
printf("require %s\n",file.c_str());
int filefd = open(file.c_str(),O_RDONLY); // 打开文件,只读
if(-1 == filefd){
perror("open file error");
return;
}
struct stat s; // 结构体,st_size能够获取文件大小
fstat(filefd,&s);
sendfile(connfd,filefd,0,s.st_size); // 可以直接发文件
// write(connfd,s.c_str(),s.size()+1);
close(filefd);
}
close(connfd);
} );
t.detach();
}
close(fd);
}
客户端:下载
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
int main(int argc,char* argv[]){
if(3!=argc){
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd){
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2]));
int res = connect(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res){
perror("connect error");
return 1;
}
// 接收管道执行信息
for(;;){
char buffer[1025] = {0}; // 多申请一个/0
int n = read(fd,buffer,1024);
// write(STDOUT_FILENO,buffer,n); // 无缓存的方式写,解决乱码问题
cout << buffer;
cout.flush(); // 手动刷数据
if(n < 1024) break;
}
string s;
while(cin >> s){ // 能够多次下载
write(fd,s.c_str(),s.size()+1);
int pos = s.find_last_of('/'); // 找到最后文件名
if(pos != string::npos){ // 如果找到了,进行切割
s = s.substr(pos);
}
string file = "./"+s; // 当前路径下加文件名
cout << file << endl;
int filefd = open(file.c_str(),O_CREAT|O_WRONLY,0666); // 创建一个文件
if(-1 == filefd){
perror("open file error");
return 1;
}
for(;;){
char buffer[256] = {0};
int n = read(fd,buffer,256);
write(filefd,buffer,n); // 读到的内容写到刚创建的文件里面
if(n < 256) break;
}
close(filefd);
}
close(fd);
}
结果为:
- 先打开服务端
- 再开服务端输入要下载的文件
- 在当前目录经能看到这个文件,可以重复下载