头文件:#include<sys/types.h>
#include
函数名:socket
函数功能:创建一个通信端口
参数1:int domain :使用的协议族
参数2:type :套接字的类型
参数3:protocol :默认为0
返回值:int 成功-> 创建的新的套接字的文件描述符,失败->-1和错误代码
int socket(int domain , int type , int protocol);
是通信协议族,使用Lunix命令【man socket】可以查看他的手册:
为了实现稳定的TCP通信,传入AF_INET,标识使用IPv4网络协议。
SOCK_STREAM:流式套接字,TCP使用
SOCK_DGRAM:数据报套接字,UDP使用
头文件:#include<sys/types.h>
#include
函数名: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地址
};
listen本质上是一个监听队列,在accept接收客户端连接之前,对将要链接的套接字进行标记。如果监听队列已经满了,再有新的客户端发起连接请求时,则无法监听,此时客户端可能会收到连接被拒绝的错误。
服务器端成功建立套接字并与地址进行绑定后,调用listen函数,将套接字标记为被动(监听模式)。准备接收客户端的连接请求
头文件:#include<sys/types.h>
#include
函数名:listen
参数1:int sockfd:监听套接字文件描述符(即socket创建,被bind绑定的套接字)
参数2:int backlog:监听队列的大小
返回值:int 成功返回0, 失败返回-1和错误代码
int listen(int sockfd , int backlog);
头文件:#include<sys/types.h>
#include
函数名: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函数
头文件:#include<sys/types.h>
#include
函数名:send
函数功能:通过socket发送数据
参数1:int sockfd:通信套接字的文件描述符
参数2:const void *buf :被发送的数据的首地址
参数3: sizeof_t len:想要发送的字节数(数据长度)
参数4:int flags:默认为0
返回值:ssize_t 类型。成功->返回成功发送的字节数,失败->返回-和错误代码
ssize_t send(int sockfd , const void *buf , size_t len , int flags);
头文件:#include<sys/types.h>
#include
函数名:recv
函数功能:通过socket接收数据
参数1:int sockfd :通信套接字的文件描述符
参数2:const void *buf:用来存储接收的数据的内存首地址
参数3:size_t len :想要接收的字节数(数据长度)
参数4:int flags:默认为0
返回值:ssize_t类型;成功->返回成功接收道德字节数,失败->返回-1
和错误码;0:表示对端执行了一个有序关闭。
通信结束,关闭连接
如果malloc申请了空间,则需要free释放
如果打开了文件,则需要关闭fd
最后close(套接字)
客户端的搭建较为简单,只需四个步骤:socket-connect-send/recv-close
头文件:#include <sys/types.h>
#include
函数功能:发起一个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);
介绍完客户-服务器的搭建流程,开始写代码
//服务器端模块化
//编写一子函数,实现监听客户端连接服务器
//参数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;
}
}
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;
}
【net.h】
#ifndef _NET_H
#define _NET_H
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#endif
代码中用到了一些字符串处理函数:例如strstr()、atoi()、
以及网络IP地址转换函数:htonl()或是htons()
char *strstr(const *str1 , const char *str2)
//从str1中寻找字符串str2第一次出现的位置
//atoi()
#include
#include
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这一块确实重要,需要认真学习。我自己也做了一点点笔记,如有需要程序源码和思维导图,请留言邮箱,我必回复。