目录
1. 客户端与服务器通信
-
server
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main()
{
int sockfd = -1, clifd = -1;
#第一步:创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
#第二步:绑定套接字
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0填充
server_addr.sin_family = PF_INET; //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(1234); //端口
bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr));
#第三步:进入监听状态
listen(sockfd, 100); // 阻塞等待客户端来连接服务器
struct sockaddr_in client_addr;
socklen_t len = 0;
char buffer[100] = {0}; //缓冲区
while(1)
{
#第四步:accept阻塞等待客户端接入
clifd = accept(sockfd, (struct sockaddr *)&client_addr, &len);
printf("连接已经建立,client fd = %d.\n", clifd);
// 建立连接之后就可以通信了
int strLen = recv(clifd, buffer, sizeof(buffer), 0);//接收客户端发来的数据
printf("客户端发送过来的内容是:%s\n", buffer);
send(clifd, buffer, strLen, 0); //将数据原样返回
memset(buffer, 0, sizeof(buffer)); //重置缓冲区
close(clifd); //关闭套接字
}
//关闭套接字
close(sockfd);
return 0;
}
-
client
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 100
int main()
{
int sockfd = -1;
char bufSend[BUF_SIZE] = {0};
char bufRecv[BUF_SIZE] = {0};
while(1)
{
#第1步:创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
#第2步:connect链接服务器
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0填充
server_addr.sin_family = PF_INET; //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(1234); //端口
connect(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("成功建立连接\n");
// 建立连接之后就可以开始通信了
// 获取用户输入的字符串并发送给服务器
printf("请输入要发送的内容: ");
fgets(bufSend, 20, stdin); //从输入流stdin,即输入缓冲区中读取20个字符到数组
send(sockfd, bufSend, strlen(bufSend), 0);
//接收服务器传回的数据
recv(sockfd, bufRecv, BUF_SIZE, 0);
//输出接收到的数据
printf("服务器返回的内容是: %s\n", bufRecv);
memset(bufSend, 0, BUF_SIZE); //重置缓冲区
memset(bufRecv, 0, BUF_SIZE); //重置缓冲区
close(sockfd); //关闭套接字
}
return 0;
}
输出如下:
> ./server
连接已经建立,client fd = 4.
客户端发送过来的内容是:wddyyds
连接已经建立,client fd = 4.
客户端发送过来的内容是:hello world !
连接已经建立,client fd = 4.
> ./client
成功建立连接
请输入要发送的内容: wddyyds
服务器返回的内容是: wddyyds
成功建立连接
请输入要发送的内容: hello world !
服务器返回的内容是: hello world !
成功建立连接
请输入要发送的内容:
- server.cpp 中调用 close() 不仅会关闭服务器端的 socket,还会通知客户端连接已断开,客户端也会清理 socket 相关资源,所以 client.cpp 中需要将 socket() 放在 while 循环内部,因为每次请求完毕都会清理 socket,下次发起请求时需要重新创建。
断开TCP连接问题:
调用 close()/closesocket() 函数意味着完全断开连接,即不能发送数据也不能接收数据,这种“生硬”的方式有时候会显得不太“优雅”。
上图演示了两台正在进行双向通信的主机。主机A发送完数据后,单方面调用 close()/closesocket() 断开连接,之后主机A、B都不能再接受对方传输的数据。实际上,是完全无法调用与数据收发有关的函数。
一般情况下这不会有问题,但有些特殊时刻,需要只断开一条数据传输通道,而保留另一条。
使用 shutdown() 函数可以达到这个目的,它的原型为:
#include <sys/socket.h>
int shutdown(int sockfd, int how);
sockfd:需要断开的套接字;
how:断开方式;
- SHUT_RD:断开输入流。套接字无法接收数据(即使输入缓冲区收到数据也被抹去),无法调用输入相关函数。
- SHUT_WR:断开输出流。套接字无法发送数据,但如果输出缓冲区中还有未传输的数据,则将传递到目标主机。
- SHUT_RDWR:同时断开 I/O 流。相当于分两次调用 shutdown(),其中一次以 SHUT_RD 为参数,另一次以 SHUT_WR 为参数。
确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
2. socket 编程实现文件传输功能
编写这个程序需要注意两个问题:
- 1) 文件大小不确定,有可能比缓冲区大很多,调用一次 write()/send() 函数不能完成文件内容的发送。接收数据时也会遇到同样的情况。
要解决这个问题,可以使用 while 循环,例如:
#Server 代码
int nCount;
while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 ){
send(sock, buffer, nCount, 0);
}
#Client 代码
int nCount;
while( (nCount = recv(clntSock, buffer, BUF_SIZE, 0)) > 0 ){
fwrite(buffer, nCount, 1, fp);
}
fread(buffer, 1, BUF_SIZE, fp) 表示么每次读取 BUF_SIZE 个字节;
返回成功读取的对象个数;当读取到文件末尾,fread() 会返回 0;
对于 Server 端的代码,当读取到文件末尾,fread() 会返回 0,结束循环。
对于 Client 端代码,有一个关键的问题,就是文件传输完毕后让 recv() 返回 0,结束 while 循环。
然而读取完缓冲区中的数据 recv() 并不会返回 0,而是被阻塞,直到缓冲区中再次有数据。
- 2) Client 端如何判断文件接收完毕,也就是上面提到的问题——何时结束 while 循环。
recv() 返回 0 的唯一时机就是收到 FIN 包时。FIN 包表示数据传输完毕,计算机收到 FIN 包后就知道对方不会再向自己传输数据,当调用 read()/recv() 函数时,如果缓冲区中没有数据,就会返回 0,表示读到了”socket文件的末尾“。
这里我们调用 shutdown() 来发送FIN包:server 端直接调用 close() 会使输出缓冲区中的数据失效,文件内容很有可能没有传输完毕连接就断开了,而调用 shutdown() 会等待输出缓冲区中的数据传输完毕。
-
server
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024
int main()
{
/**先检查文件是否存在**/
char *filename = "/home/wdd/test/2/video.mp4"; //文件名
FILE *fp = fopen(filename, "rb"); //以二进制方式打开文件
if(fp == NULL)
{
printf("Cannot open file, press any key to exit!\n");
return 0;
}
int sockfd = -1, clifd = -1;
//第一步:创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//第二步:绑定套接字
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0填充
server_addr.sin_family = PF_INET; //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(1234); //端口
bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr));
//第三步:进入监听状态
listen(sockfd, 100); // 阻塞等待客户端来连接服务器
//第四步:接收客户端请求
struct sockaddr_in client_addr;
socklen_t len = 0;
clifd = accept(sockfd, (struct sockaddr *)&client_addr, &len); //accept阻塞等待客户端接入
printf("连接已经建立,client fd = %d.\n", clifd);
/**循环发送数据,直到文件结尾**/
char buffer[BUF_SIZE] = {0}; //缓冲区
int nCount;
while( (nCount = fread(buffer, 1, BUF_SIZE, fp)) > 0 )
{
send(clifd, buffer, nCount, 0); //发送数据到客户端
}
//文件读取完毕,断开输出流
//当输出缓冲区中的数据传输完毕,再向客户端发送FIN包
shutdown(clifd, SHUT_WR);
//阻塞,等待客户端接收完毕
recv(clifd, buffer, sizeof(buffer), 0);
//连接已中止,recv 返回0
printf("客户端接收完毕 !\n");
fclose(fp);
close(clifd); //关闭套接字
close(sockfd); //关闭套接字
return 0;
}
-
client
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024
int main()
{
//先输入文件名,看文件是否能创建成功
char filename[100] = {0}; //文件名
printf("Input filename to save: ");
fgets(filename, 20, stdin);
FILE *fp = fopen(filename, "wb"); //以二进制方式打开(创建)文件
if(fp == NULL)
{
printf("Cannot open file, press any key to exit!\n");
exit(0);
}
int sockfd = -1;
//第1步:创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//第2步:connect链接服务器
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr)); //每个字节都用0填充
server_addr.sin_family = PF_INET; //使用IPv4地址
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); //具体的IP地址
server_addr.sin_port = htons(1234); //端口
connect(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr));
printf("成功建立连接\n");
//循环接收数据,直到文件传输完毕
char buffer[BUF_SIZE] = {0}; //文件缓冲区
int nCount;
while( (nCount = recv(sockfd, buffer, BUF_SIZE, 0)) > 0 )
{
fwrite(buffer, nCount, 1, fp);//将每次结合搜的数据写入上面创建的文件
}
//运行到此,说明输出缓冲区接收完毕,客户端收到 FIN 包
printf("文件传输完毕 !\n");
//文件接收完毕后直接关闭套接字,无需调用shutdown()
fclose(fp);
close(sockfd); //关闭套接字
return 0;
}
输出:
-> ./server
连接已经建立,client fd = 5.
客户端接收完毕 !
-> ./client
Input filename to save: da hua xi you
成功建立连接
文件传输完毕 !