4. 基于TCP的服务器端/客户端(1)
4.1 TCP和UDP
TCP套接字是面向连接的,因此又称基于流(stream)的套接字。
TCP,Transmission Control Protocol,传输控制协议
四层网络模型
四层TCP/IP协议栈
链路层:最底层,定义LAN 、WAN 、MAN等网络标准
数据链路层通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路,为IP 层提供数据传送服务。
IP层:传输路径选择。IP本身是面向消息的、不可靠的协议。lP协议无法应对数据错误。
网络层通过路由选择算法,为分组选择最适当的路径,实现两个端系统之间的数据透明传送。
TCP/UDP:以IP层提供的路径信息完成实际的数据传输。IP只是负责传输数据包,不关心顺序、丢失或损坏的情况,TCP解决这些问题
应用层:在进行网络编程时,前3层的实现都被隐藏在socket之中,不需要我们多加操作。利用套接字编写程序,根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这便是应用层协议。网络编程的大部分内容就是设计并实现应用层协议。
4.2 简单的TCP服务器端/客户端
以前面的 hello_server.c 服务器端和 hello_client.c 客户端为例,说明这个流程(注意顺序)
(1)服务器端:调用 socket() 和 bind() 创建套接字并为套接字分配地址信息
(2)服务器端:调用 listen() 进入等待连接请求状态
只有服务器端调用了listen函数,客户端才能进入可发出连接请求的状态(调用connect())。若客户端提前调用connect()将报错
listen()并不代表建立连接,它只是创建了一个连接请求队列,这个队列由第2个参数backlog决定大小。当客户端发起连接请求时,listen将其加入到连接请求队列之中,等待着连接受理。
(3)客户端:调用 connect() 发出连接请求(默认已用socket()创建套接字)
调用connect()之后,只有服务器端“接收连接请求”或者“发生断网等异常情况而中断连接请求”后才能返回值
这里的“接收连接”并不是指服务器端调用accept(),而是指请求被listen()记录到了等待队列之中
因此connect()返回后,并不能立即进行数据交换,因为此时连接尚未受理
(4)服务器端:调用 accept() 受理客户端连接请求
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen)
sock选择监听套接字;addr保存客户端地址信息。
accept()受理sock创建的等待队列中的连接请求。它将自动创建一个新的套接字,并自动与该客户端建立连接,数据传输。
如果此时等待队列中为空,accept()将不会返回(阻塞),直到有连接请求到来为止
为什么要新建套接字:sock本身是一个监听套接字,如果将它用于和客户端的通信,那么它就无法正常接收新的连接请求了(难以区分哪些是请求连接的套接字信息,哪些是传输的数据),因此对于每一个客户端的连接请求,都要新建一个对应的套接字。
(5)服务器端/客户端:调用 read() write() 传输数据
accept()后,就有了一对一用于传输数据的套接字。此时,服务器端和客户端就可以向指定的socket进行读写了
(6)服务器端/客户端:close() 关闭套接字
思考:为什么客户端不用bind()为套接字分配地址信息?
客户端使用socket()之后立即使用connect(),似乎并未替socket分配地址信息。但实际上,网络数据交换的双方必须都分配IP地址和端口号。客户端的IP地址和端口号在调用connect()时由操作系统自动分配,它的IP地址即是自身计算机IP地址,端口号随机分配。
connect()的第1个参数是自己的sockfd,这个sockfd会被自动绑定客户端的IP地址和端口号;第2个参数就是服务器端的IP地址和端口号
(客户端连接服务器端时,需要服务器端的端口号。服务器端回复客户端时,也需要客户端的端口号。这两个端口号不一致)
4.3 迭代服务器端/客户端(echo)
服务器端和客户端不断互传信息,直到客户端输入Q为止
注意:此时服务器端在同一时刻只能处理一个客户端 —— 依次处理5个客户端的请求
服务器端 echo_server.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
void error_handling(const char* message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int clnt_addr_size;
int i, str_len;
char message[BUF_SIZE];
//1. 从这里开始和之前的 hello_server.c 一样
if(argc != 2)
{
error_handling("wrong argc");
exit(1);
}
//socket
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error!");
//bind
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error!");
//listen
if(listen(serv_sock, 5) == -1)
error_handling("listen() error!");
//2. 使用循环,依次接受accept
clnt_addr_size = sizeof(clnt_addr);
for(i=0; i<5; ++i){
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error!");
else
printf("Connected client %d \n", i+1);
//反复接收数据,再返回给客户端;每次读取BUF_SIZE个字节
while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
close(clnt_sock);
}
close(clnt_sock);
return 0;
}
客户端 echo_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
char message[BUF_SIZE];
int str_len;
//1. 前面的部分和hello_client.c差不多
if(argc != 3)
{
error_handling("wrong argc");
exit(1);
}
//socket
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error!");
//connect
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error");
else
puts("Connected ...");
//若调用该函数引起的连接请求被注册到服务器端等待队列,则connect函数将完成正常调用。
//即使输出了“connected ...”,但如果服务器尚未调用accept函数,也不会真正建立服务关系。
//2. 反复发送数据,再反复接收
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
//strcmp相等返回0
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);//调用close()向服务器发送EOF(意味着中断连接)
return 0;
}
运行结果
echo服务器端/客户端存在的问题:
场景:服务器端从socket中读取数据,每次读多少就发送给客户端多少。设想当数据量很大的情况,此时服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整
解决:下一章详细说明
思考:关于accep()
首先,服务器端创建 serv_sock,这个serv_sock绑定了服务器端的ip地址和设置的端口号
然后,客户端创建 sock,这个sock绑定了自己的地址和端口,通过connect发送到服务器端
服务器端 accept 接收到请求,解析客户端 sock 中的地址和端口,并创建一个新的 clnt_sock
服务器端创建的serv_sock和clnt_sock,文件描述符fd分别是3,4
客户端创建的sock,文件描述符为3
服务器端和客户端创建的3个socket是不一样的
服务器端读写在clnt_sock,客户端读写在sock
问题:clnt_sock是怎么和sock传输数据的?(socket连接)
windows平台下的echo
(1)修改头文件,添加 WSAStartup() 语句
(2)修改变量类型名字,如socket的返回值类型要从 int 修改为 SOCKET
(3)bind()函数的返回值判断由 -1 修改为 SOCKET_ERROR
(4)read/write 修改为 send/recv (参数变为4个);close变为closesocket
都是一些小的修改,和前面linux下的代码差不多的。
5. 基于TCP的服务器端/客户端(2)
5.1 echo存在的问题和客户端修改
回顾前面的迭代服务器端/客户端的问题:无法达到TCP无数据边界的要求
如数据太大,服务器端需要多次write才能发送完毕。客户端有可能在服务器端write完毕之前就调用了read(),导致读取不完整
服务器端收发数据的代码:
//反复读取,每次读多少就返回给客户端多少
while((str_len=read(clnt_sock, message, BUF_SIZE)) != 0)
write(clnt_sock, message, str_len);
客户端收发数据的代码:
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
//重点语句
//write写数据没有问题,每次将字符串写完
write(sock, message, strlen(message));
//read的问题:假设服务器端发送的慢,一次read不能读完整怎么办?
//服务器端没有这个问题是因为,服务器端用的是while+read,而不是一次性read
//服务器端不断read/write,不关心每次传输的数据大小。客户端应当确定每次read多少,每次write多少
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
一种修改办法,是在客户端读取时,while循环+指定读取大小,如下
//控制客户端读取的字节数
str_len = write(sock, message, strlen(message));
recv_len = 0;
while(recv_len < str_len)
{
recv_cnt = read(sock, &message[recv_len], BUF_SIZE-1);
if(recv_cnt == -1)
error_handling("read() error!")
recv_len += recv+cnt;
}
message[recv_len] = 0;
这种方式需要客户端自己清楚要接收的大小(这种很多情况做不到)。
5.2 应用层协议层面上进行修改
收发数据过程中定好规则(协议)以表示数据的边界,或提前告知收发数据的大小。服务器端/客户端实现过程中逐步定义的这些规则集合就是应用层协议。例如:“收到Q就终止连接”
在应用层协议中可以定好数据边界的表示方法、数据的长度范围等。
案例:客户端向服务器端发送加减乘除的运算要求,服务器端计算结果并返回给客户端
自行尝试(跳过)
(之后再回来看存在哪些问题)
op_server.c
clnt_addr_size = sizeof(clnt_addr);
for(i=0; i<5; ++i)
{
num_count = 0;
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
printf("client fd: %d\n", clnt_sock);
while(1)
{
if((read_len=read(clnt_sock, buf, BUF_SIZE)) != 0)//0表示到达结尾
{
if(read_len == -1)
error_handling("read() error");
if(buf[0]<'0' || buf[0]>'9')
break;
nums[num_count++] = atoi(buf);
printf("%d\n", nums[num_count-1]);
}
else
break;
}
op = buf[0];
printf("op is %c\n", op);
res = 0;
switch(op)
{
case '+':
for(j=0; j<num_count; ++j)
res+=nums[j];
break;
default:
printf("no such operator!\n");
break;
}
printf("return result: %d\n", res);
putchar('\n');
sprintf(buf, "%d", res);
write(clnt_sock, buf, sizeof(buf));
close(clnt_sock);
}
op_client.c
fputs("Operand count: ", stdout);
scanf("%d", &num_count);
getchar();
for(i=0; i<=num_count; ++i){
if(i<num_count)
printf("Operand %d: ", i+1);
else
printf("Operator: ");
fgets(message, BUF_SIZE, stdin);
write(sock, message, strlen(message));
}
str_len = read(sock, message, BUF_SIZE-1);
message[str_len] = 0;
printf("Message from server: %s\n", message);
书本代码(协议方式)
制定协议
定义协议如下:
(1)客户端连接到服务器端后以1字节整数形式传递待运算数字个数
(2)客户端向服务器端传递的每个整数型数据占用4字节
(3)传递整数型数据后接着传递运算符。运算符信息占用1字节
(4)选择字符+,-,*之一传递
(5)服务器端以4字节返回运算结果
(6)客户端得到运算结果之后终止与服务器端的连接
若想在数组中保存并传输多种类型,应当声明为 char 类型
!!从协议的角度解决问题时,尽可能从指针和字节的角度理解read和write,像 int 和 char 这样的类型理解为 4字节 和 1字节
协议就是规定双方的读写规则
op_client.c
先看客户端,它要做更多的事情
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4
#define RLT_SIZE 4
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
int opnd_cnt, i;
char opmsg[BUF_SIZE];
int result;
if(argc != 3)
{
error_handling("wrong argc");
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error");
else
puts("Connected ...");
//从这里开始修改
fputs("Operand count: ", stdout);
scanf("%d", &opnd_cnt);
//协议1:用1个字节保存运算数的个数,最高为255个,即 0b11111111,更大的会被截断
opmsg[0] = (char)opnd_cnt;
for(i=0; i<opnd_cnt; ++i)
{
printf("Operand %d: ", i+1);
//协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节
scanf("%d", (int *)&opmsg[i*OPSZ+1]);//这行代码详解放在了下面
}
//协议3:用1个字节保存运算符+、-、*
fgetc(stdin);//读取字符时要注意缓冲区残留的'\n'问题
fputs("Operator: ", stdout);
scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);
//协议4:服务器端返回的结果用4个字节保存
write(sock, opmsg, opnd_cnt*OPSZ+2);
read(sock, &result, RLT_SIZE);//RLT_SIZE也是4字节,result是int类型
printf("Message from server: %d\n", result);
close(sock);
return 0;
}
关于scanf读取int型数据,但存放在 char str[] 数组的过程:
scanf("%d", (int *)&opmsg[i*OPSZ+1]);//每个运算数不管大小要占4个字节
//相当于以int读取一个数,例如384,它占4个字节,即32bit,二进制是 (0b)00000000 00000000 00000001 10000000
//将这4个字节放到 &opmsg[i*OPSZ+1] 这一地址指向的内存空间,因为opmsg是char类型数组,一个元素占1字节,因此要占4个元素
//假设下标从1开始,从数值的角度来看,则opmsg[1]=0b10000000=-128, opmsg[2]=0b00000001=1, opmsg[3]=0, opmsg[4]=0;
//即使不用(int *)显式说明也是可以的
//服务器端再将opmsg转为int型(例如一次性读4个字节),就能得到对应的int数值
op_server.c
和客户端采用同样的协议规则,重点在几条协议的实现代码上
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
#define OPSZ 4
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int calculate(int opnum, int opnds[], char op)
{
int result = opnds[0], i;
switch(op)
{
case '+':
for(i=1; i<opnum; ++i) result+=opnds[i];
break;
case '-':
for(i=1; i<opnum; ++i) result-=opnds[i];
break;
case '*':
for(i=1; i<opnum; ++i) result*=opnds[i];
break;
default:
printf("no such operator!");
break;
}
return result;
}
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int i, j, clnt_addr_size;
char opinfo[BUF_SIZE];
int opnd_cnt, recv_len, recv_cnt;
int result;
if(argc != 2)
{
error_handling("wrong argc");
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
for(i=0; i<5; ++i)
{
opnd_cnt = 0;
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
printf("connect socket: fd = %d\n", clnt_sock);
//协议1:用1个字节保存运算数的个数。这里将1字节的内容转给4字节的int类型
read(clnt_sock, &opnd_cnt, 1);
//下面服务端将所有传输过来的内容放到opinfo里面
recv_len = 0;
//opnd_cnt*OPSZ+1 表示剩下还有多少字节没有读取
while((opnd_cnt*OPSZ+1)>recv_len)
{
recv_cnt = read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
recv_len += recv_cnt;
}
//协议2: 用4个字节保存每一个运算数,OPSZ为自定义的4字节
//协议3:用1个字节保存运算符+、-、*
//这里用int *强转为int arr[],arr的最后一个元素是没有意义的运算符
result=calculate(opnd_cnt, (int *)opinfo, opinfo[recv_len-1]);
printf("return value = %d\n", result);
//协议4:服务器端返回的结果用4个字节保存
write(clnt_sock, (char *)&result, sizeof(result));
close(clnt_sock);
}
close(serv_sock);
return 0;
}
运行结果
5.3 TCP套接字的I/O缓存
I/O缓冲
- I/O缓冲在每个TCP套接字中单独存在
- I/O缓冲在创建套接字时自动生成
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据
- 关闭套接字将丢失输入缓冲中的数据
TCP使用滑动窗口,保证每次传输的数据不会超过接收方的输入缓冲的剩余空间大小
write/send 函数返回的时间节点:在数据移动到发送方的输出缓冲后即返回,TCP会保证输出缓冲中的数据传输给接收方
TCP内部工作原理1:三次握手
套接字是以全双工( Full-duplex )方式工作的。也就是说,它可以双向传递数据
以A向B发起连接为例
(1)第1次 A --> B
[SYN] SEQ: 1000, ACK: -
SEQ sequence,表示序号;代表这是当前A传输给B的数据包,序号是1000
ACK 确认消息,表示希望从对方那里收到的下一个数据包序号。- 表示空,意味着这是首次连接
“现传递的数据包序号为1000 ,如果接收无误,请通知我向您传递1001号数据包。”
(2)第2次 B --> A
[SYN+ACK] SEQ: 2000, ACK: 1001
SEQ 代表这是B传给A的数据包,序号是2000
ACK 确认消息,表示之前已经收到过A传给B的1000,希望下次收到的是1001
“现传递的数据包序号为2000 ,如果接收无误,请通知我向您传递2001 号数据包。“
(3)第3次 A --> B
[ACK] SEQ: 1001, ACK 2001
含义与第2次类似。TCP保证了有序传输
到这里,A和B各发送了一次确认消息ACK,确认了彼此均就绪
TCP内部工作原理2:数据交换
每次收到的确认号 ACK = SEQ + 传输的字节数 + 1
加上传输的字节数,是为了检查是否所有数据都被收到了
最后加1,是为了告知对方下次希望收到的 SEQ
TCP处理传输错误的情况:
接收方每收到一个数据包,都会发送ACK确认,如果数据包丢失,就不会发送ACK。
发送方发送一个数据包后,超过一定时间还没有收到ACK,就认为数据包丢失,于是重传数据包
(即便发送方超时重传后又收到了第一次发送的ACK,也没有关系。因为接收方收到重复的数据包会舍弃,被舍弃的数据包不会为它发送ACK)
快速重传:
A发送 S1, S2, S3, S4 共4个数据包给B,S1顺利到达,于是B返回确认号 A2;
S2延迟或丢失,于是S3和S4先一步到达。此时发现还是没有S2,于是发出的确认号都是 A2。
A收到3个一样的确认号 A2,就知道S2缺失,于是发送S2
TCP内部工作原理3:断开连接
FIN表示断开连接,双方各发送一次FIN,并且各收到一次ACK后,连接断开,又称为四次握手
注意的是,中间主机B连续两次发送数据包
主机B的FIN中,再一次使用 ACK 5001 是表示上一次只是为了接收ACK,并没有接收A发来的数据,所以要求A重传一次
5.4 练习
收发文件的服务器端和客户端
- 客户端接受用户输入的传输文件名。
- 客户端请求服务器端传输该文件名所指文件。
- 如果指定文件存在,服务器端就将其发送给客户端;反之,则断开连接。
存在的问题:当服务器端和客户端之间有数据传输时,服务器端调用close(),客户端接收到的是-1,而不是0
原因:只要TCP栈的读缓冲里还有未读取(read)数据,则调用close时会直接向对端发送RST,而不是FIN
解决方法:在close()之前,使用shutdown(clnt_sock, SHUT_WR)。详见第7章
file_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#define BUF_SIZE 1024
void error_handling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
int i, clnt_addr_size, file_fd, read_len;
char buf[BUF_SIZE];
char file_name[BUF_SIZE];
if(argc != 2)
{
error_handling("wrong argc");
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
if(listen(serv_sock, 5) == -1)
error_handling("listen() error");
clnt_addr_size = sizeof(clnt_addr);
for(i=0; i<5; ++i)
{
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size);
printf("connected client fd = %d\n", clnt_sock);
if(clnt_sock == -1)
error_handling("accept error");
read(clnt_sock, file_name, BUF_SIZE-1);
printf("file name = %s\n", file_name);
file_fd = open(file_name, O_RDONLY);
if(file_fd == -1)
{
close(clnt_sock);
fputs("can not find the file!", stdout);
break;
}
while((read_len = read(file_fd, buf, BUF_SIZE-1)) > 0)
{
printf("read size: %d\n", read_len);
write(clnt_sock, buf, read_len);
}
close(file_fd);
close(clnt_sock);
}
close(serv_sock);
return 0;
}
file_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#define BUF_SIZE 1024
#define NAME 64
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
int main(int argc, char* argv[])
{
int sock;
struct sockaddr_in serv_addr;
int recv_len, i, file_fd;
char buf[BUF_SIZE];
char file_copy[BUF_SIZE];
if(argc != 3)
{
error_handling("wrong argc");
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(sock == -1)
error_handling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
error_handling("connect() error");
else
puts("Connected ...");
fputs("enter the file name: ", stdout);
scanf("%s", file_copy);
write(sock, file_copy, sizeof(file_copy));
strcat(file_copy, "_copy.c");
while((recv_len = read(sock, buf, BUF_SIZE-1)) != 0)
{
if(recv_len == -1)
{
error_handling("read() error!");
}
//注意,如果用open是新建一个文件,最好加上八进制的权限设置,如0644(权限设置和linux文件操作一样)
file_fd = open(file_copy, O_CREAT|O_APPEND|O_WRONLY, 0644);
write(file_fd, buf, recv_len);
close(file_fd);
}
close(sock);
return 0;
}