<>TCP/IP通信:实现客户-服务器之间的通信、文件上传与下载

<>通信流程:

<>(一)搭建服务器(server)

<>1. socket 创建套接字
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:socket 函数功能:创建一个通信端口 参数1:
int domain :使用的协议族 参数2:type :套接字的类型 参数3:protocol :默认为0 返回值:int 成功->
创建的新的套接字的文件描述符,失败->-1和错误代码 int socket(int domain , int type , int protocol);
<>参数1:通信协议

是通信协议族,使用Lunix命令【man socket】可以查看他的手册:

<>

为了实现稳定的TCP通信,传入AF_INET,标识使用IPv4网络协议。

<>参数2:数据传输方式/套接字类型

SOCK_STREAM:流式套接字,TCP使用
SOCK_DGRAM:数据报套接字,UDP使用

<>参数3:protocol:默认为0

<>2. bind 绑定
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:bind
函数功能:给socket绑定IP和端口(需要被找到的套接字才需要被绑定) 参数1:int sockfd:创建出来的新的套接字的文件描述符 参数2:const
struct sockaddr *addr:存储自己的IP地址、端口的结构的结构体首地址 参数3:socklen_t addrlen :
addrlen结构体的大小 返回值:int 成功->返回0,失败->-1和错误代码 int bind(int sockfd , const struct
sockaddr*addr , socklen_t addrlen); struct sockaddr { sa_family_t sa_family;
//地址族 char sa_data[14]; //14字节,包含套接字中的目标地址和端口信息 };
需要注意的是:bind主要用于服务器端,客户端创建的套接字可以不绑定地址
TCP搭建服务器使用的地址结构并不是struct sockaddr , 而是struct sockaddr_in
struct sockaddr_in { short int sin_family; //网络协议 unsigned short int sin_port;
//端口号 struct in_addr sin_addr; //32位IP地址 };
sockaddr_in结构体内还包含一个结构体:struct in_addr
struct in_addr { unit32_t s_addr; //32位IP地址 };
<>3. listen 监听

listen本质上是一个监听队列,在accept接收客户端连接之前,对将要链接的套接字进行标记。如果监听队列已经满了,再有新的客户端发起连接请求时,则无法监听,此时客户端可能会收到连接被拒绝的错误。

服务器端成功建立套接字并与地址进行绑定后,调用listen函数,将套接字标记为被动(监听模式)。准备接收客户端的连接请求
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:listen 参数1:int sockfd:
监听套接字文件描述符(即socket创建,被bind绑定的套接字) 参数2:int backlog:监听队列的大小 返回值:int 成功返回0, 失败返回-1
和错误代码int listen(int sockfd , int backlog);
<>4. accept 连接
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:accpet 函数功能:接收一个套接字的连接请求
参数1:int sockfd :监听套接字的文件描述符(经过listen的sockfd) 参数2:struct sockaddr *addr :
用来存储对方(客户端)地址结构的内存首地址 【对方的IP地址和端口号】 参数3:socklen_t *addrlen:存储地址结构信息长度变量的地址
返回值:成功->通信套接字的文件描述符,失败返回-1和错误代码 int accept(int sockfd , struct sockaddr *addr ,
socklen_t*addrlen);
accept接受连接的方式有两种:
1:不关心客户端的IP地址和端口号时,参数2和参数3设置为NULL
2:若要读取并保存客户端的IP地址和端口号,则需要新建一个socket_in结构体

做完前四步后就可以进行通信了。TCP/IP提供了一种通信方式:send函数和recv函数

<>

<>5. send
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:send 函数功能:通过socket发送数据 参数1
:int sockfd:通信套接字的文件描述符 参数2:const void *buf :被发送的数据的首地址 参数3: sizeof_t
len:想要发送的字节数(数据长度) 参数4:int flags:默认为0 返回值:ssize_t 类型。成功->返回成功发送的字节数,失败->返回-
和错误代码 ssize_tsend(int sockfd , const void *buf , size_t len , int flags);
<>6. recv
头文件:#include<sys/types.h> #include<sys/socket.h> 函数名:recv 函数功能:通过socket接收数据 参数1
:int sockfd :通信套接字的文件描述符 参数2:const void *buf:用来存储接收的数据的内存首地址 参数3:size_t len
:想要接收的字节数(数据长度) 参数4:int flags:默认为0 返回值:ssize_t类型;成功->返回成功接收道德字节数,失败->返回-1 和错误码;0
:表示对端执行了一个有序关闭。
<>7. close

通信结束,关闭连接
如果malloc申请了空间,则需要free释放
如果打开了文件,则需要关闭fd
最后close(套接字)

<>以上时搭建TCP服务器的流程,一共分为7个步骤,其中前三个步骤可以打包为一个子函数。

<>(二)搭建客户端(client)

客户端的搭建较为简单,只需四个步骤:socket-connect-send/recv-close

<>1. socket ---->参考上面服务器介绍

<>2.connect
头文件:#include <sys/types.h> #include <sys/socket.h> 函数功能:发起一个socket的连接请求 参数1:int
sockfd:客户端socket函数创建的套接字的文件描述符 参数2:const struct sockaddr *addr:
存储服务器的IP地址和端口结构的内存首地址 参数3:socklen_t addrlen:addr只想的内存空间大小 返回值:int类型:成功->返回0,失败->
返回-1和错误码 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
;
<>3.send /recv ---->参考上面服务器介绍

<>4.close

介绍完客户-服务器的搭建流程,开始写代码

<>代码实现

<>server【服务器端】
//服务器端模块化 //编写一子函数,实现监听客户端连接服务器 //参数1:IP地址 char * ip //参数2:端口号 int port
//返回值:成功->返回监听的socket对象,失败返回-1 int tcp_server(char * ip , int port); //函数实现 nt
tcp_server(char * ip , int port) //子函数实现 { //1.创建socket对象TCP //2.绑定自己的IP地址&端口号
//3.监听是否有人链接 //1.创建socket对象TCP int tcp_fd = -1; // tcp_fd = socket(AF_INET ,
SOCK_STREAM, 0); //1.1判断是否绑定成功 if(0 > tcp_fd) { perror("socket error!"); return
-1; } puts("socket success!"); //2.绑定自己的IP地址&端口号 //结构体见顶部注释 struct sockaddr_in
myaddr; //定义一个结构体myaddr,数据类型为struct sockaddr_in myaddr.sin_family = AF_INET;
//协议 myaddr.sin_port = htons(port); //端口号,atoi()意思是将主机字节序转换为网络字节序 myaddr.
sin_addr.s_addr = inet_addr(ip);//IP地址 //2.2bind if(bind(tcp_fd , (struct
sockaddr*)&myaddr , sizeof(myaddr)) < 0) { perror("bind error!"); //打印错误原因 close
(tcp_fd); //关闭 return -1; } puts("bind success!"); //3.监听是否有人链接 if(0 > listen(
tcp_fd, 5)) //参数2:监听队列的大小,一般设置为5 { perror("listen error!"); close(tcp_fd);
return -1; } puts("listen success!"); return tcp_fd; //监听成功,返回socket对象tcp_fd }
接下来时accept和通信
//编写一子函数,和客户端进行通信 //参数:accept后新生成的socket对象 //返回值:void int
tcp_server_communiction(int acp_fd); //函数实现 int tcp_server_communiction(int
acp_fd) { //5.1:收 //5.1.1:入参判断 if(0 > acp_fd) { perror("This acp_fd is NULL!");
return -1; } //5.1.2: acp_fd的格式正确,首先接受client客户端发送的数据 char buf[1024];
//定义一个数组,存储收发的文件 memset(buf , 0 , sizeof(buf)); //清空数组 //read(lst_fd , buf ,
sizeof(buf)); //将客户端发送的数据通过IO文件操作中的read方式读入到该数组中 //换第二种文件操作方式:
send和recv,这是TCP/IP自带的一种文件收发方式 int rec = -1; //定义一个变量rec用于接收recv()函数的返回值 rec =
recv(acp_fd , buf , sizeof(buf) , 0);//执行接收函数 //5.1.3:判断是否接收成功 if(0 > rec) {
perror("recv error!"); return -1; } //接收正常,但还要判断是否为空数据 if(0 < buf) {
//证明数组中接收到了客户端发来的数据 puts("buf"); char ncmd[10] = {0};
//用来存放客户端的操作方式:是上传[up]还是下载[down] char filename[64] = {0}; //用于存放客户端要操作的对象名字
//还需要定义一个子函数,以解析客户端发来的命令 analysis_cmd(buf , ncmd , filename);
//调用命令解析函数,将客户端发送的命令和文件名解析出来,并且存储到前面定义的ncmd[]和filename[]数组中 printf("%s\n" , ncmd
); printf("%s\n" , filename); //成功拿到命令和文件名 //下载文件 if(0 == strcmp(ncmd , "down"))
//调用字符串比较函数,两个字符串相同则返回0 { //下载文件 int fd = open(filename , O_RDONLY);
//fn:文件名,O_RDONLY:以只读的方式打开文件 //判断文件是否打开成功 if(0 > fd) {
//fd为open函数的返回值,小于0表明文件打开失败 char msg[30] = "文件不存在";
//调用send函数,将这个字符串发送给client客户端 send(acp_fd , msg , sizeof(msg) , 0); return -1; }
else { //文件打开成功 int len = lseek(fd , 0 , SEEK_END); printf("要发送的文件长度为:%d\n" ,
len); char fl[30] = {0}; sprintf(fl , "lend:%d" , len);//将计算到的文件长度输出重定向到fl[]数组中
//将文件长度先返回给客户端,以便于客户端创建一个同等大小的数组用于接收文件 send(acp_fd , fl , sizeof(fl) , 0);//发送
//然后准备发送文件 //由于lseek已经指向文件末尾了,所以需要让他回到文件开头 lseek(fd , 0 , SEEK_SET);
//SEEK_SET:文件的起始位置 /*大文件需要在栈区申请空间*/ char *picture = NULL; //创建一个图片指针 picture = (
char *)malloc(len); //为这个指针开辟一片内存空间 //判断是否开辟成功 if(NULL == picture) { perror(
"malloc error!"); return -1; } //开辟成功 //清空 memset(picture , 0 , sizeof(picture))
; int red = read(fd , picture , len); //判断是否读取文件成功 if(len == red)
//read()函数,读取文件成功时,会返回文件长度 { //将读取到的文件数据发送给client客户端 send(acp_fd , picture ,
sizeof(picture) , 0); } sleep(5); free(picture); close(fd); } } else if(0 ==
strcmp(ncmd , "up")) { //上传文件 //首先创建一个数组用来接收客户端发来的文件大小信息 char msg[30] = {0}; int
ret= -1; ret = recv(acp_fd , msg , sizeof(msg) , 0); //接收客户端的文件信息 //判断 if(0 <
ret) { puts(msg); //客户发来的数组fl已经接收成功,打印出来 //解析字符串中的数字 } if(strstr(msg , "lenth"))
//通过字符串定位函数,定位到数字的位置 { int len = atoi(msg + 6);//提取字符串中的数字
//printf("文件大小为:%d\n" , len); int fd = -1; // //调用open函数创建一个文件,大小为提取出的len fd =
open(filename , O_WRONLY | O_CREAT , 0777); if(0 > fd) { puts("open | creat
file error!"); close(fd); return -1; } //接收文件,大文件需要存放在堆区,使用maloc申请,使用完需手动释放 char
*file = NULL; file = (char *)malloc(len); if(NULL == file) { puts("malloc error"
); close(fd); return -1; } //大文件无法一次接收完,需要循环接受 int t = 0; int rec = -1; while(t
< len) { memset(file , 0 , len); rec = recv(acp_fd , file , len-t , 0); if(0 <
rec) { write(fd , file , rec); t += rec; } sleep(1); } puts("文件接收成功!"); free(
file); close(fd); } } } //5.2:发 /*memset(buf , 0 , sizeof(buf)); //先清空存储数据的数组
printf("write:"); fgets(buf , sizeof(buf) , stdin); //标准输入流:键盘获取数据到存储数据的数组中
write(acp_fd , buf , strlen(buf)); close(acp_fd); */ }
最后是字符串分析函数,通过此函数可以将客户端发来的【命令 文件】分离出来
臂如:down 1.jpg 说明客户端要从服务器下载一张名为1.jpg的图片
//定义一个子函数,解析从客户端接受到的命令 //从该命令(str字符串)中,解析出操作方式up 或 down // 解析出要操作的对象 filename
int analysis_cmd(char *str , char *cmd , char *filename); //参数1:从客户端接收到的字符串
//参数2:用于存储解析出的命令 //参数3:用于存储解析出的文件名 //返回值:无 //函数实现 int analysis_cmd(char *str ,
char *cmd , char *filename) //解析命令 { //1.入参判断 if(NULL == str || NULL == cmd ||
NULL == filename) { perror("参数不匹配,请重新输入!"); return -1; } char *p = str;
//定义一个指针p指向接收的字符串str的首地址 while(*p) { if(' ' == *p) //命令与文件名之间有一个空格 { break;
//跳出循环 } else { *cmd = *p; //从str的起始位置开始,将p指向的字符依次复制cmd cmd += 1;
//cmd是一个存储命令的数组,数组名就是数组的首地址,所以cmd+1,表示指针向后移动一个位置 }
//跳出while循环时表明已经将命令遍历出来了[解析出操作方式:up或down] p += 1; //p指针向后移动 } strcpy(filename ,
p+1); //这里的p还指在命令与文件名间的空格处,所以需要再向后移动一个位置才是文件名的首地址
//使用strcpy函数将p指针之后的字符复制给存储filename的数组 }
最后是主函数,调用
int main(int argc , char *argv[]) { //0.判断main函数传参是否正确 if(2 > argc) { perror(
"请输入正确的参数:IP地址 端口号"); return -1; } //1.监听socket对象 int tcp_fd = -1; tcp_fd =
tcp_server(argv[1] , atoi(argv[2])); while(1)
//accept,用于将客户端的链接请求与服务器建立TCP通信,并返回一个新的已经建立链接的accept套接字 { //2.接受链接
//若不关心客户端的IP地址和端口号,则将参数2,3设置为空 //4.1:定义一个保存客户端IP地址和端口号的结构体并清空 int lst_fd = -1;
struct sockaddr_in client; //创建一个结构体 memset(&client , 0 , sizeof(client));
//4.2:定义一个int型变量,保存结构体的大小 int len = sizeof(client); //4.3:接受连接,并且保存客户端的信息 lst_fd
= accept(tcp_fd , (struct sockaddr *)&client , &len);
//4.4:将网络字节序ip地址转换为主机字节序ip地址 char * ip = inet_ntoa(client.sin_addr);
//4.5:网络字节序port转换为主机字节序port unsigned short port = ntohs(client.sin_port);
//4.6:打印出客户端的ip地址和端口号 printf("client IP = %s , PORT = %d \n" , ip , port);
//lst_fd = accept(tcp_fd , NULL , NULL); //不需要获取客户端的ip地址和端口号 //4.7判断是否链接成功 if(0
> lst_fd) { perror("accept error!"); close(tcp_fd); return -1; } puts("accept
success!"); //do_work() //2.和客户端进行通信 tcp_server_communiction(lst_fd);
//6.关闭socket对象 close(tcp_fd); return 0; } }
<>client【客户端】

socket+connect,返回connect成功的套接字
//定义一个tcp连接的子函数 //参数1:ip地址 char * //参数2:端口号 int port
//返回值:成功->返回链接好的socket对象,失败返回-1 int tcp_connect(char * ip , int port); //函数实现
int tcp_connect(char * ip , int port) //子函数实现 { //1.创建TCP socket //2.设置对方的IP和端口号
//3.请求链接 //1.创建TCP socket int tcp_fd = -1; //初始化为-1 tcp_fd = socket(AF_INET ,
SOCK_STREAM, 0); //IPV4,流式套接字 //1.1 判断是否创建成功 if(0 > tcp_fd) { perror("socket
error!"); return -1; } puts("socket success!"); //2.设置对方的IP和端口号[bind] //2.1
定义一个结构体变量 struct sockaddr_in myser; //类似于c++的实例化对象 // myser.sin_family = AF_INET
; //协议 myser.sin_addr.s_addr = inet_addr(ip); myser.sin_port = htons(port);
//主机字节序转网络字节序 //3.请求链接 int ret = -1; ret = connect(tcp_fd , (struct sockaddr *)&
myser, sizeof(myser)); //connect连接 if(0 != ret) { perror("connect error!");
close(tcp_fd); return -1; } puts("connect success!"); return tcp_fd;
//返回connect成功后新生成的套接字 }
和服务器进行通信
//定义一个子函数,和服务器端进行通信 //参数:创建好的socket对象 //返回值:void int tcp_client_communiction(
int tcp_fd); // //函数实现 /*do_work*/ int tcp_client_communiction(int tcp_fd)
//子函数:和服务器进行通信的实现 { //入参判断 if(0 > tcp_fd) { printf("%s < 0 , connot to
communictioning with server!\n" , tcp_fd); return -1; }
//4.发送消息:给server发送下载或上传指令 char buf[50]; memset(buf , 0 , sizeof(buf)); gets(buf)
; //输入 send(tcp_fd , buf , strlen(buf) , 0); char ncmd[10] = {0}; char filename[
64] = {0}; analysis_cmd(buf , ncmd , filename); //puts(ncmd); //puts(filename);
//sleep(2); if(0 == strcmp(ncmd , "down")) { //send后准备recv,接受服务器的文件
//定义一个数组,用来接受server发过来的文件长度message char msg[30] = {0}; int ret = -1; ret = recv(
tcp_fd, msg , sizeof(msg) , 0); puts(msg); if(0 < ret) { //接收命令成功 puts(buf);
//打印buf里接收到的文本字符串[待接收的文件大小] //判断是否是server发来的错误信息 if(0 == strcmp(msg , "文件不存在"))
{ return 0; } else if(strstr(msg , "len")) { int len = atoi(msg + 5);
//提取字符串中的数字 printf("文件长度为:%d\n" , len); //用open函数新建一个文件,大小为len int fd = -1; fd =
open(filename , O_WRONLY | O_CREAT , 0777); //判断文件描述符是否创建成功 if(0 > fd) { puts(
"open|creat file error!"); close(fd); return -1; } //接收文件 char *file = NULL;
file= (char *)malloc(len); //判断文件接收空间是否申请成功 if(NULL == file) { puts("malloc
error!"); close(fd); return -1; } int rec = -1; //一次接收不完,需要循环接收 int t = 0; while
(t < len) { memset(file , 0 , len); rec = recv(tcp_fd , file , len-t , 0);
//recv的成功的返回值是接受到文件的大小 //判断是否接受成功 //printf("%d\n" , rec); if(0 < rec) {
//puts("进来了"); write(fd , file , rec); t += rec; } //printf("%d\n",t); sleep(1);
} puts("文件接收成功!"); free(file); close(fd); } } else if(0 == ret) //没有接受到任何字符 {
return 0; } } else if(0 == strcmp(ncmd , "up")) { //上传文件 //文件操作,打开文件 int fd =
open(filename , O_RDONLY); //以只读的方式打开文件 //judge if(0 > fd) {
//由于是客户向服务器上传文件,所以不必告诉服务器文件是否上传错误,只需告诉客户端就行 puts("文件打开失败,请重新上传!"); close(fd);
return -1; } //打开成功,需要计算文件长度并发送给服务器 int lenth = -1; lenth = lseek(fd , 0 ,
SEEK_END); //调用lseek函数,从0位置跳到文件末尾,计算出文件长度 printf("主人,您要上传的文件大小为:%d\n" , lenth);
char fl[30] = {0}; sprintf(fl , "lenth:%d" , lenth); //将字符串输出重定向到数组中,发送该数组给服务器
send(tcp_fd , fl , sizeof(fl) , 0); //准备发送文件 lseek(fd , 0 , SEEK_SET);
//回到文件起始位置 char *file = NULL; file = (char *)malloc(lenth); //申请空间,将图片存放于此,等待发送
if(NULL == file) { perror("malloc error!"); return -1; } memset(file , 0 , lenth
); int red = (fd , file , lenth); if(red == lenth) { //读取成功,准备向服务器发送 send(tcp_fd
, file , lenth , 0); } sleep(5); free(file); close(fd); } return 0; }
同样,客户端也需要一个字符串解析函数,所以直接拷贝服务器里面的函数
//字符串解析函数,解析出文件大小 int analysis_cmd(char *str , char *cmd , char *filename)
//解析命令 { //1.入参判断 if(NULL == str || NULL == cmd || NULL == filename) { perror(
"参数不匹配,请重新输入!"); return -1; } char *p = str; //定义一个指针p指向接收的字符串str的首地址 while(*p)
{ if(' ' == *p) //命令与文件名之间有一个空格 { break; //跳出循环 } else { *cmd = *p;
//从str的起始位置开始,将p指向的字符依次复制cmd cmd += 1;
//cmd是一个存储命令的数组,数组名就是数组的首地址,所以cmd+1,表示指针向后移动一个位置 }
//跳出while循环时表明已经将命令遍历出来了[解析出操作方式:up或down] p += 1; //p指针向后移动 } strcpy(filename ,
p+1); //这里的p还指在命令与文件名间的空格处,所以需要再向后移动一个位置才是文件名的首地址
//使用strcpy函数将p指针之后的字符复制给存储filename的数组 }
最后是main函数的调用:
int main(int argc , char *argv[]) { //0.首先判断传参是否格式正确 if(2 > argc) { printf(
"传入参数不够,请重新传参!"); return -1; } //1.连接服务器 int tcp_fd = -1; tcp_fd = tcp_connect(
argv[1] , atoi(argv[2])); //atoi()将传入的端口号[例如:8888]转化为网络字节序 //do_work
//2.和服务器端进行通信 tcp_client_communiction(tcp_fd); //3.关闭socket对象 close(tcp_fd);
return 0; }
<>为了模块化编写程序,我们还需要把各种库函数封装成一个.h文件

【net.h】
#ifndef _NET_H #define _NET_H #include<sys/types.h> #include<sys/socket.h> #
include<netinet/in.h> #include<netinet/ip.h> #include<string.h> #include
<unistd.h> #include<stdlib.h> #include<arpa/inet.h> #include<stdlib.h> #include
<fcntl.h> #endif
代码中用到了一些字符串处理函数:例如strstr()、atoi()、
以及网络IP地址转换函数:htonl()或是htons()
char *strstr(const *str1 , const char *str2) //从str1中寻找字符串str2第一次出现的位置
结果就是:程序会从第一个xiao处打印后面的字符串

//atoi() #include<stdio.h> #include<stdlib.h> int main() { char arr1[] =
"-1234"; char arr2[] = "len:569456"; printf("%d\n" , atoi(arr1)); printf("%d\n"
, atoi(arr2+4));//提取字符串中的数字 return 0; }

<>程序测试

在不同的文件夹中分别运行客户服务器端的程序,设定IP地址为本地回环地址:127.0.0.1
端口号:8888
分别在服务器目录下存放一张照片,在客户端目录下存放一个.txt文档,然后测试。
首先是客户端需要将服务器的图片下载至自己的目录下,然后再从自己目录上传一个文档至服务器。

<>连接成功

<>首先是图片下载成功

<>文件上传成功

结束!

一点点感悟,网编流程基本不变,服务器与客户端的搭建模式只要记清,剩下的就是文件操作了,只是需要考虑的一些细节很多,稍不注意可能会有Bug,没错,我的程序也有Bug,文件下载完成后客户端会进入最后的while死循环出不来,目前还没检查出来问题所在。TCP/IP这一块确实重要,需要认真学习。我自己也做了一点点笔记,如有需要程序源码和思维导图,请留言邮箱,我必回复。

技术
下载桌面版
GitHub
Gitee
SourceForge
百度网盘(提取码:draw)
云服务器优惠
华为云优惠券
腾讯云优惠券
阿里云优惠券
Vultr优惠券
站点信息
问题反馈
邮箱:[email protected]
吐槽一下
QQ群:766591547
关注微信