Linux之网络编程
TCP协议
TCP(Transmission ControlProtocol) : 传输控制协议,是一个
面向连接的、可靠的、基于字节流的传输层的协议。TCP 协议建立的是一种点到点的,一对一的可靠连接协议
特点:
- 数据无丢失
- 数据无失序
- 数据无错误
- 数据无重复
使用场景
- 适合于对传输质量要求较高,以及传输大量数据的通信。
- 在需要可靠数据传输的场合,通常使用 TCP 协议
- MSN/QQ 等即时通讯软件的用户登录账户管理相关的功能通常采用 TCP 协议
面向连接,数据可靠
- 三次握手,本意指 TCP/IP 协议栈中,要求 TCP 协议提供可靠的连接服务。我们需要建立可靠的,稳定的链接的时候,我们就需要使用三次握手。它的实质是指建立一个TCP 连接的时候,客户端和服务端需要发送 3 个数据包。
- 四次挥手,当用户想要断开连接的时候,需要发送四次数据包,才会中断连接。
TCP 源端口(Source Port): 源计算机上的应用程序的端口号,占 16 位。
TCP 目的端口(Destination Port): 目标计算机的应用程序端口号,占 16 位
数据序号 (Sequence Number,seq):它表示本报文段所发送数据的第一个字节的编号。在 TCP 连接中,所传送的字节流的每一个字节都会按顺序编号。当SYN 标记不为 1 时,这是当前数据分段第一个字母的序列号;如果 SYN 的值是 1时,这个字段的值就是初始序列值(ISN),用于对序列号进行同步。这时,第一个字节的序列号比这个字段的值大 1,也就是 ISN 加 1
确认序号 (Acknowledgment Number,ack) :占 32bit, 它表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。其值是接收计算机即将接收到的下一个序列号,也就是下一个接收到的字节的序列号加 1。
TCP 首部长度(Header Length):数据偏移是指数据段中的 “数据” 部分起始处距离 TCP 数据段起始处的字节偏移量,占 4 位。其实这里的 “数据偏移” 也是在确定TCP 数据段头部分的长度,告诉接收端的应用程序,数据从何处开始。
保留 (Reserved): 占 4 位;为 TCP 将来的发展预留空间,目前必须全部为 0
标志位字段:
U——URG,表示本报文段中发送的数据是否包含紧急数据:URG=1 时表示有紧急数据。当 URG=1 时,后面的紧急指针字段才有效。
A——ACK,表示前面的确认号字段是否有效:ACK=1 时表示有效;只有当 ACK=1时,前面的确认号字段才有效;TCP 规定,连接建立后,ACK 必须为 1
P——PSH, 告诉对方收到该报文段后是否立即把数据推送给上层。如果值为 1,表示应当立即把数据提交给上层,而不是缓存起来
R——RST,表示是否重置连接:若 RST=1,说明 TCP 连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接
S——SYN,在建立连接时使用,用来同步序号:当 SYN=1,ACK=0 时,表示这是一个请求建立连接的报文段;当 SYN=1,ACK=1 时,表示对方同意建立连接;SYN=1时,说明这是一个请求建立连接或同意建立连接的报文;
只有在前两次握手中 SYN 才为 1
F——FIN,标记数据是否发送完毕:若 FIN=1,表示数据已经发送完成,可以释放连接
窗口大小 (Window Size): 占 16 位;它表示从 Ack Number 开始还可以接收多少字节的数据量,也表示当前接收端的接收窗口还有多少剩余空间。该字段可以用于 TCP 的流量控制。
校验和 (TCP Checksum): 占 16 位;它用于确认传输的数据是否有损坏。发送端基于数据内容校验生成一个数值,接收端根据接收的数据校验生成一个值。两个值必须相同,才能证明数据是有效的。如果两个值不同,则丢掉这个数据包。Checksum 是根据伪头 + TCP 头 + TCP 数据三部分进行计算的。
紧急指针 (Urgent Pointer): 仅当前面的 URG 控制位为 1 时才有意义。它指出本数据段中为紧急数据的字节数,占 16 位;当所有紧急数据处理完后,TCP 就会告诉应用程序恢复到正常操作。即使当前窗口大小为 0,也是可以发送紧急数据的,因为紧急数据无须缓存。
选项 (Option): 长度不定,但长度必须是 32bits 的整数倍;选项中的内容不确定,因此就必须使用首部长度来区分选项具体的长度.
填充字段 (Fill): 这是为了使整个首部长度是 4 个字节的倍数。IP 数据报的首部也同样有这个字段,也要 4 字节对齐
三次握手
第一次握手,由客户端发送请求连接即 SYN = 1,TCP 规定 SYN=1 的时候,不能够携带数据。但是需要消耗一个 seq 序号。因此,产生了一个序号 seq = x.
第二次握手,然后 B 主机收到 A 主机发送的消息。向 A 主机发送确认。发送 SYN = 1, 表示请求连接已经收到,然后发送确认 ACK=1,把 TCP 包中 ACK 位置为 1. 在来发送一个新的序列号 seq =y,确认好 ack = x + 1;
第三次握手,其实经过两次连接之后,双方的基本连接已经建立。但是 A 收到 B 的确认之后,还需要向 B 给出确认,说明自己已经收到确认包了。设置确认 ACK = 1,ack = y + 1. 而顺序号 seq =x + 1,。双方建立稳定的连接。此时 ACK 报文可以携带数据。
四次挥手
第一次挥手:
客户端打算关闭连接,此时会发送⼀个 TCP⾸部 FIN 标志位被置为 1 的报⽂,也即FIN 报文,之后客户端进⼊ FIN_WAIT_1 状态。
第二次挥手:
服务端收到该报⽂后,就向客户端发送 ACK 应答报⽂,接着服务端进⼊CLOSED_WAIT 状态。
第三次挥手:
客户端收到服务端的 ACK 应答报⽂后,之后进⼊ FIN_WAIT_2 状态。但此时服务端可能还有一些数据未处理完。等待服务端处理完数据后,也向客户端发送 FIN 报⽂,之后服务端进⼊ LAST_ACK 状态。
第四次挥手:
客户端收到服务端的 FIN 报⽂后,回⼀个 ACK 应答报⽂,之后进⼊ TIME_WAIT 状态服务端收到了 ACK 应答报⽂后,就进⼊了 CLOSED 状态,⾄此服务端已经完成连接的关闭。
客户端在经过 2MSL ⼀段时间后,⾃动进⼊ CLOSED 状态,⾄此客户端也完成连接的关
UDP协议
特点
- UDP 是无连接的协议。
- UDP 使用尽最大努力交付,不保证数据可靠。
- UDP 是面向报文的。
- UDP 通信的实时性较高。
使用场景
- 发送小尺寸数据(如对 DNS 服务器进行 IP 地址查询时)
- 在接收到数据,给出应答较困难的网络中使用 UDP。(如:无线网络)
适合于广播 / 组播式通信中。 - MSN/QQ/Skype 等即时通讯软件的点对点文本通讯以及音视频通讯通常采用 UDP 协议
- 流媒体、VOD、VoIP、IPTV 等网络多媒体服务中通常采用 UDP 方式进行实时数据传输
字节序转换 API
1、IP 字符串转换为网络字节序
方法1
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>typedef unsigned int uint32_t;
typedef unsigned int in_addr_t;in_addr_t inet_addr(const char *cp);
功能:将cp指向的IP字符串转成网络字节序 返回值:成功返回网络字节序,失败返INADDR_NONE[0xffffffff]
注意:它不能识别255.255.255.255
方法2
int inet_aton(const char *cp, struct in_addr *inp);
[addr to network]
功能:将cp指向的IP字符串转成网络字节inp保存的地址中。
参数:@cp IP字符串首地址 @inp 存放网络字节序的地址
返回值:成功返回非0,失败返回0 struct in_addr {
unsigned int s_addr;
};
实质:存储在inp结构体指针中的 s_addr这个变量
2、网络字节序转换为IP字符串
char *inet_ntoa(struct in_addr in);
[network to addr]功能:将IP网络字节序转换成IP字符串
参数:@in IP网络字节序
返回值:成功返回IP字符串首地址,失败返回NULL
3、端口转换为网络字节序
short htons(short data);
[host to network short ]功能:将short类型的整数转成网络字节序
参数: @data 序号转换的整数返回值:得到的网络字节序
4、网络字节序转换为端口
uint32_t ntohs(uint32_t netlong);
[network to host short]功能:把网络字节序转换为主机端口
参数:@ netlong 网络字节序
返回值: 返回对应的主机端口
示例:
写一个代码实现以下功能
1.用户从命令行传递参数 ./a.out 127.0.0.1 9090
2.利用 inet_aton 和 htons 函数把 ip,port 转换为网络字节序后输出。
3.利用 inet_ntoa 和 noths 函数把 ip,port 转换为主机字节序后输出
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
void ip_to_network01(const char *cp){
in_addr_t net_addr;
net_addr=inet_addr(cp);
if(net_addr==INADDR_NONE){
perror("[ERROR] inet_addr():" );
exit(EXIT_FAILURE);
}printf("%x\n",net_addr);}void ip_to_network02(const char *cp){struct in_addr addr;int ret=inet_aton(cp,&addr);if(ret==0){perror("[ERROR] inet_aton:");exit(EXIT_FAILURE);}printf("network=%x\n",addr.s_addr);printf("addr=%s\n",inet_ntoa(addr));}void port_to_network(short data){short result=htons(data);
printf("network port=%d\n",result);printf("port=%d\n",ntohs(result));}int main(int argc, const char *argv[])
{ip_to_network01(argv[1]);printf("-------------\n");ip_to_network02(argv[1]);
port_to_network(atoi(argv[2]));return 0;
}
UDP通信流程
介绍用到的函数
1、创建套接字
int socket(int domain, int type, int protocol);
参数: @domain 地址族 :AF_UNIX 本地unix域通信 ,AF_INET IPV4 ineter网通信 [我们使用这个]
@ type :使用协议类型 SOCK_STREAM 流式套接字(TCP), SOCK_DGRAM 报文套接字(UDP) ,SOCK_RAW 原始套接字: (IP,ICMP)
@protocol 协议编号 0 : 让系统自动识别
返回值:成功返回得到的文件描述符。失败返回 -1
示例用法
int fd = socket(AF_INET,SOCK_DGRAM,0);
2、发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 参数: @sockfd 套接字
@buf 数据存放的首地址
@len 期望发送的数据大小
@flags 操作方式 0 表示默认操作
@dest_addr 向指定的地址发送数据
@addrlen 发送的地址的大小
返回值: 成功返回实际发送的字节数,失败返回-1 struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};//struct sockaddr 可强转为 struct sockaddr_in
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}; struct in_addr {
uint32_t s_addr;
};
struct sockaddr_in peer_addr;
peer_addr.sin_family = AF_INET;
peer_addr.sin_port = htons(8080);
peer_addr.sin_addr.s_addr = inet_addr("192.168.0.88");
n = sendto(sockfd,buf,n,0,(struct sockaddr *)&peer_addr,sizeof(struct sockaddr_in));
示例:udp客户端实现
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
void send_data(int sockfd,struct sockaddr_in *addr,int len){
int n=0;
char buf[1024]={0};
while(1){
printf(">");
memset(buf,sizeof(buf),0);
fgets(buf,sizeof(buf),stdin);
if(strncmp("quit",buf,4)==0){
break;
}
else{
ssize_t s=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)addr,len);//发送信息
if(s<0){
perror("[ERROR] sendto:");
}
}
}return;
}int main(int argc,const char *argv[]){
int socket_id;
struct sockaddr_in addr;
socklen_t len=sizeof(addr);
socket_id=socket(AF_INET,SOCK_DGRAM,0);
if(socket_id<0){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}
memset(&addr,sizeof(addr),0);
addr.sin_family=AF_INET;
addr.sin_port=htons(atoi(argv[2]));//网络端口
addr.sin_addr.s_addr=inet_addr(argv[1]);//网络ip
send_data(socket_id,&addr,len);return 0;
}
3、把 ip 和端口与当前进程绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
功能:把ip地址和端口绑定到socket中去。
参数:@sockfd socket创建的文件描述符
@addr 把IP和地址设置到对应的结构体中去。
@addrlen 表示 addr 参数对应类型的地址信息结构体的大小返回值
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
//struct sockaddr_in可强转为struct sockaddr
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
struct in_addr{
unsigned int s_addr;
}
返回值:成功 返回0失败返回 -1 ,并设置errno
用法:
//定义结构体
struct sockaddr_in my_addr;
memest(&my_addr,0,sizeof(my_addr)); //填充数据my_addr.sin_family = AF_INET;
my_addr.sin_port =htons(atoi(argv[2])); my_addr.sin_addr.s_addr = inet_addr(argv[1]);
//绑定数据
if(bind(sockfd,(struct sockaddr *)&my_addr),sizeof(my_addr) < 0){ ...}
4、接受数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
参数:@sockfd 套接字
@buf 数据存放的首地址
@len 期望接收的数据大小
@flags 操作方式 0 表示默认操作
@src_addr 获得发送方地址,谁发送的获得谁的地址。
@addrlen 值结果参数,必须进行初始化, 表示表示对方实际地址的大小。
返回值:成功返回实际接收的字节数,失败返回-1
示例用法:
struct sockaddr_in peer_addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
n = recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&peer_addr,&addrlen);
示例:udp服务端实现
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>void recv_data(int socketfd){int n=0;
char buff[1024]={0};
struct sockaddr_in client_addr;
socklen_t len=sizeof(client_addr);
while(1){
memset(buff,0,sizeof(buff));
n=recvfrom(socketfd,buff,sizeof(buff),0,(struct sockaddr *)&client_addr,&len);
printf("n=%d\n",n);
if(n<0){
perror("Fail to recvfrom");
exit(EXIT_FAILURE);
}
printf("=================\n");
printf("Recv from IP:%s\n",inet_ntoa(client_addr.sin_addr));
printf("Recv from port:%d\n",ntohs(client_addr.sin_port));
printf("Recv %d bytes:%s\n",n,buff);
if(strncmp(buff,"quit",4)==0){
break;
}}
return ;
}int main(int argc,const char *argv[]){
int socketfd;
struct sockaddr_in my_addr;
socklen_t len=sizeof(my_addr);
socketfd=socket(AF_INET,SOCK_DGRAM,0);
if(socketfd<0){
perror("socketfd<0");
exit(EXIT_FAILURE);
}
memset(&my_addr,0,sizeof(my_addr));
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(atoi(argv[2]));
my_addr.sin_addr.s_addr=inet_addr(argv[1]);
if(bind(socketfd,(struct sockaddr *)&my_addr,len)<0){
perror("Fail to bind!\n");
exit(EXIT_FAILURE);
}
recv_data(socketfd);
close(socketfd);return 0;
}
多进程并发
常用的服务器模型
- 迭代服务器
大多数 UDP 都是迭代运行,服务器等用客户端的数据,收到数据后处理该数据,送回其应答,在等待下一个客户请求。
socket();
bind();
while(1){
recvfrom();
process();
sendto();
}
close();
- 并发服务器:并发服务器是指在同一个时刻可以响应多个客户端的请求。
本质是创建多进程 / 多线程,对多数用户的信息进行处理。
UDP 协议一般默认是不支持多线程并发的,因为默认 UDP 服务器只有一个sockfd,
所有的客户端都是通过同一个 sockfd 进行通信的. udp 一个socket,如何做到做并发呢?
sockfd = socket();
bind();
while(1){
recvfrom(); ...
}
close();
当 UDP 协议针对客户请求的处理需要消耗过长的时间时,我们期望 UDP 服务器具有某种形式的并发
示例:
多个 udp 客户端登录用户需要先验证密钥是否正确后,才能允许用户进行数据交互。假设密钥为 “root”.
服务器接收客户端信息的时候,需要考虑两种情况
A 用户的密钥验证请求消息。
B 用户的数据交互接收消息。
多个用户之间实现并发
服务端udp_fork_server.c
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0int init_socket(const char *ip,const char *port){int sockfd=0;
struct sockaddr_in my_addr;
socklen_t len=sizeof(my_addr);sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
perror("Fail to socket!\n");
exit(EXIT_FAILURE);
}
memset(&my_addr,0,sizeof(my_addr));
my_addr.sin_family=AF_INET;
my_addr.sin_port=htons(atoi(port));
my_addr.sin_addr.s_addr=inet_addr(ip);
if(bind(sockfd,(struct sockaddr *)&my_addr,len)<0)
{
perror("Fail to bind\n");
return -1;
}
return sockfd;
}int user_login(const char *ip,const char *port){int n=0;
char buf[20]={0};
struct sockaddr_in client_addr;
socklen_t len=sizeof(client_addr);
unsigned char login_flag;
int sockfd;
int new_sockfd;sockfd=init_socket(ip,port);
while(1){
//Receive messagememset(buf,0,sizeof(buf));n=recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&client_addr,&len);if(n<0){perror("Fail to sendto");exit(EXIT_FAILURE);}printf("key=%s\n",buf);login_flag=(strncmp(buf,"root",4)==0)?LOGIN_SUCCESS:LOGIN_FAILURE;if(login_flag==LOGIN_SUCCESS){if(fork()==0){//son process close(sockfd);new_sockfd=init_socket(ip,"0");sendto(new_sockfd,&login_flag,sizeof(login_flag),0,(struct sockaddr *)&client_addr,len);break;}}else{sendto(sockfd,&login_flag,sizeof(login_flag),0,(struct sockaddr*)&client_addr,len);}}return new_sockfd;
}void printf_client_info(struct sockaddr_in *addr,char *buf){printf("===============================\n");printf("user IP : %s\n",inet_ntoa(addr->sin_addr));printf("user port : %d\n", ntohs(addr->sin_port)); printf("user data : %s\n",buf);}void recv_data(int new_sockfd){
int n=0;
char buf[1024]={0};struct sockaddr_in client_addr;
socklen_t len=sizeof(client_addr);
while(1){memset(buf,0,sizeof(buf));
n=recvfrom(new_sockfd,buf,sizeof(buf),0,(struct sockaddr *)&client_addr,&len);if(n<0){perror("Fail to sendto");
exit(EXIT_FAILURE);
}printf_client_info(&client_addr,buf);if(strncmp(buf,"quit",4)==0){
break;
}}close(new_sockfd);
exit(EXIT_SUCCESS);
return;}void sig_handler(int signum){waitpid(-1,NULL,WNOHANG);printf("recv singnum = %d zombie\n",signum);return ;
}int main(int argc,const char *argv[]){int sockfd;
unsigned char login_flag;
if(argc<3){
fprintf(stderr,"Usage :%s is port!\n",argv[0]);
exit(EXIT_FAILURE);
}if(signal(SIGCHLD,sig_handler) == SIG_ERR) {perror("Fail to single\n");return -1;
}sockfd=user_login(argv[1],argv[2]);
recv_data(sockfd);
return 0;}
udp_client.c
#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0void user_login(int sockfd,struct sockaddr_in *addr,struct sockaddr_in *new_addr,int len){int n=0;
char buf[1024]={0};
unsigned char flag=LOGIN_FAILURE;while(1){
putchar('>');
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]='\0';n=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)addr,len);recvfrom(sockfd,&flag,sizeof(flag),0,(struct sockaddr *)new_addr,&len);if(flag==LOGIN_SUCCESS){break;}}return;}void send_message(int sockfd,struct sockaddr_in *addr,int addr_len){int n = 0;char buf[1024] = {0}; while(1) { printf("Input : ");memset(buf,0,sizeof(buf));fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = '\0';n = sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)addr,addr_len); if(n < 0) {perror("Fail to sendto");exit(EXIT_FAILURE); }if(strncmp(buf,"quit",4) == 0)break;}return ;
}int main(int argc,const char *argv[]){
int socketfd=0;
struct sockaddr_in peer_addr;
struct sockaddr_in server_addr;
socklen_t len=sizeof(peer_addr);if(argc<3){fprintf(stderr,"Usage:%s is port \n",argv[0]);
exit(EXIT_FAILURE);}
socketfd=socket(AF_INET,SOCK_DGRAM,0);
if(socketfd<0){
perror("Fali to socket");
exit(EXIT_FAILURE);
}memset(&peer_addr,0,sizeof(peer_addr));
peer_addr.sin_family=AF_INET;
peer_addr.sin_port=htons(atoi(argv[2]));
peer_addr.sin_addr.s_addr=inet_addr(argv[1]);
memset(&server_addr,0,sizeof(server_addr));
user_login(socketfd,&peer_addr,&server_addr,len);send_message(socketfd,&server_addr,len);
close(socketfd);return 0;
}
TCPSocket编程
客户端连接
在整个流程中, 主要涉及以下几个接口
socket() : 创建套接字, 使用的套接字类型为流式套接字
connect() : 连接服务器
send() : 数据发送
recv() : 数据接收
1、创建套接字的函数为 socket ,具体信息如下:
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int socket(int domain,int type,int protocol)
函数功能:创建套接字
函数参数
domain: 协议族 ,AF_INTE
type : 套接字类型SOCK_STREAM:流式套接字, 传输层使用 tcp 协议SOCK_DGRAM: 数据包套接字, 传输层使用 udp 协议protocol : 协议, 可以填 0
函数返回值
成功 : 返回 套接字文件描述符
失败 : 返回 -1,并设置 errno
2、连接服务器要调用的函数为connect , 具体如下:
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
函数功能:发起对套接字的连接 (基于面向连接的协议)
函数参数
sockfd : 套接字文件描述符
addr : 连接的套接字的地址结构对象的地址 (一般为服务器)
internet 协议族使用的 struct sockaddr_in 结构体,大小与通用struct sockaddr 结构体一致
addrlen : 地址结构的长度
函数返回值
成功 : 返回 0
失败 : 返回 -1,并设置 errno
示例:TCP连接实现客户端
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
int main(int argc,const char *argv[]){
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in svr_addr;
bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
int ret=connect(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));if(ret==-1){
perror("[ERROR] connect():");
exit(EXIT_FAILURE);}close(sockfd);return 0;}
bzero()函数: 能够将内存块(字符串)的前n个字节清零
头文件
#include <string.h>
原型为:
void bzero(void *s, int n);
//【参数】s为内存(字符串)指针,n 为需要清零的字节数。
客户端发送与接受数据
基于 socket 发送数据需要调用send函数, 下面是 send 函数的具体信息
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t send(int sockfd,const void *buf,size_t len,int flags)
函数功能:基于套接字(建立连接)发送数据
函数参数
sockfd : 套接字文件描述符
buf : 发送缓冲区的地址
len : 发送数据的长度
flags : 发送标志位
函数返回值
成功 : 返回 成功发送的字节数
失败 : 返回 -1, 并设置 errno
基于 socket 接收数据需要调用recv函数, 具体信息如下:
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t recv(int sockfd,void *buf,size_t len,int flags)
函数功能:基于套接字接收数据
函数参数
sockfd : 套接字文件描述符
buf : 接收缓冲区的地址
len : 接收数据最大长度
flags : 标志位
函数返回值
成功 : 返回 成功接收的字节数
案例:客户端接受与发送文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
int main(int argc,const char *argv[]){
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in svr_addr;
bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
int ret=connect(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));if(ret==-1){
perror("[ERROR] connect():");
exit(EXIT_FAILURE);}
char buff[64]={"hello server"};char recv_buff[64]={0};
ssize_t wbytes=send(sockfd,buff,strlen(buff),0);
if(wbytes==-1){
perror("[ERROR] send:");
exit(EXIT_FAILURE);
}ssize_t rbytes=recv(sockfd,recv_buff,sizeof(recv_buff),0);//注意是sizeof
printf("recv data:%s\n",recv_buff);close(sockfd);return 0;}
服务端连接
socket() : 创建套接字, 使用的套接字类型为流式套接字
bind : 绑定 ip 地址与端口号,用于客户端连接服务器
listen : 建立监听队列,并设置套接字的状态为 listen 状态, 表示可以接收连接请求
accept : 接受连接, 建立三次握手, 并创建新的文件描述符, 用于数据传输
recv() : 数据接收
send() : 数据发送
close():关闭socket
socket 套接字状态如下:
CLOSED : 关闭状态
SYN-SENT : 套接字正在试图主动建立连接 [发送 SYN 后还没有收到 ACK],很短暂
SYN-RECEIVE : 正在处于连接的初始同步状态 [收到对方的 SYN,但还没收到自己发过去的SYN 的 ACK]
ESTABLISHED : 连接已建立
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型:
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
函数功能:绑定 ip 地址与端口号
函数参数
sockfd : 套接字文件描述符
buf : 接收缓冲区的地址
len : 接收数据最大长度
flags : 标志位
函数返回值
成功 : 返回 成功接收的字节数
失败 : 返回 -1, 并设置 errno
在服务器绑定 ip 地址与端口号之后, 则需要让服务器 socket 套接字设置成被动监听状态,并创建监听队列,这里需要调用listen函数
listen
函数的详细信息如下:
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int listen(int sockfd,int backlog)
函数功能:设置套接字状态为被动监听,并创建监听队列
函数参数:
sockfd : 套接字文件描述符
backlog : 监听队列的长度
函数返回值
成功 : 返回 0
失败 : 返回 -1, 并设置 errno
在 tcp 服务器中 设置套接字为监听状态, 并创建监听队列
当 客户端 发出连接请求之后, 则需要调用accept函数建立连接
accept 函数具体信息如下:
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
函数功能:接受来自于其他 socket 的连接请求,并建立连接
函数参数
sockfd : 套接字文件描述符
addr : 网络地址结构的指针(输出参数,用于保存发送请求端的地址信息)
addrlen : 网络地址结构长度的指针 (输出参数,但是需要进行初始化)
函数返回值
成功 : 返回新的文件描述符
失败 : -1 , 并设置 errno
示例:设计一个服务器程序,并和客户端建立连接,并打印客户端的 ip 地址和端口号
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define LISTEN_SZ 10int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);
bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));for(;;){}
close(accept_fd);
close(sockfd);
return 0;
}
tcp 服务器数据接收与发送都是使用send函数与recv函数
示例 : 实现 echo 服务器(将客户端发送的数据再重新发送给客户端)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define LISTEN_SZ 10int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;char buffer[64]={0};sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);
bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));for(;;){ssize_t rbytes = recv(accept_fd,buffer,sizeof(buffer),0);
if (rbytes == -1){perror("recv(): ");exit(EXIT_FAILURE);
}else if (rbytes == 0){//说明客户端下线了printf("The client is offline.\n"); exit(EXIT_FAILURE);
}else if (rbytes > 0){ ssize_t sbytes = send(accept_fd,buffer,sizeof(buffer),0);if (sbytes == -1){perror("[ERROR] send(): ");exit(EXIT_FAILURE);}
}
}
close(accept_fd);
close(sockfd);
return 0;
}
TCP粘包
粘包演示:
tpc_server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define LISTEN_SZ 10int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;char buffer[64]={0};sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);
bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));for(;;){ssize_t rbytes = recv(accept_fd,buffer,sizeof(buffer),0);if (rbytes == -1){perror("recv(): ");exit(EXIT_FAILURE);
}else if (rbytes == 0){//说明客户端下线了printf("The client is offline.\n"); exit(EXIT_FAILURE);
}else if (rbytes > 0){ printf("buffer:%s\n",buffer);}sleep(1);//睡眠1秒
}
close(accept_fd);
close(sockfd);
return 0;
}
tcp_client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
int main(int argc,const char *argv[]){
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in svr_addr;
bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
int ret=connect(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));if(ret==-1){
perror("[ERROR] connect():");
exit(EXIT_FAILURE);}
char buff[64]={"hello server"};char recv_buff[64]={0};for(;;){
ssize_t wbytes=send(sockfd,buff,strlen(buff),0);
if(wbytes==-1){
perror("[ERROR] send:");
exit(EXIT_FAILURE);
}
usleep(100);//睡眠100u秒<1秒}
close(sockfd);return 0;}
解决TCP粘包
方式一 : 使用定长数据包, 每次必须要读取固定长度的数据,适用于数据长度是固定的场景
方式二 : 使用数据长度 + 数据的方式,先接收数据长度,再根据长度接收数据, 这里就结合第一种方式,进行固定长度接收,这种方式适用于不定长数据场景。
方式三 : 使用特殊间隔符,如换行等来区分数据包的边界, 使用较少。
这里使用第二种方式,前四个字节保存长度
tcp_client.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
int main(int argc,const char *argv[]){
int sockfd=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in svr_addr;
bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
int ret=connect(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));if(ret==-1){
perror("[ERROR] connect():");
exit(EXIT_FAILURE);}
char buff[64]={"hello server"};
int length;
char recv_buff[64]={0};
char *pbuff;for(;;){length=strlen(buff);
pbuff=(char *)malloc(length+4);
memcpy(pbuff,&length,4);
memcpy(pbuff+4,buff,length);ssize_t wbytes=send(sockfd,pbuff,length+4,0);if(wbytes==-1){
perror("[ERROR] send:");
exit(EXIT_FAILURE);
}
usleep(100);}
close(sockfd);return 0;}
tcp_server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define LISTEN_SZ 10int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;char buffer[64]={0};
ssize_t rbytes;
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);
bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));int length,total_received;
for(;;){
length=0;
total_received=0;rbytes = recv(accept_fd,&length,4,0);for(;;){rbytes=recv(accept_fd,buffer+total_received,length-total_received,0);if (rbytes == -1){perror("recv(): ");exit(EXIT_FAILURE);
}else if (rbytes == 0){//说明客户端下线了printf("The client is offline.\n"); exit(EXIT_FAILURE);
}else if (rbytes > 0){
total_received+=rbytes;
if(total_received==length){
break;
}}
}printf("buffer:%s\n",buffer);
sleep(1);
}
close(accept_fd);
close(sockfd);
return 0;
}
强化TCPSocket的使用
TCP并发服务器多进程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<wait.h>#define LISTEN_SZ 10//使用信号量等待子进程退出,防止阻塞
void do_sigchld_handler(int sig){wait(NULL);
}void do_client(int cfd){
char buffer[1024]={0};
ssize_t sbytes,rbytes;
__sighandler_t retsig;
retsig=signal(SIGCHLD,do_sigchld_handler);
if(retsig==SIG_ERR){
perror("[ERROR] signal():");
exit(EXIT_FAILURE);
}
memset(buffer,0,sizeof(buffer));
rbytes=recv(cfd,buffer,sizeof(buffer),0);
if(rbytes==-1){
perror("recv():");
exit(EXIT_FAILURE);
}else if(rbytes>0){sbytes=send(cfd,buffer,sizeof(buffer),0);}close(cfd);
exit(EXIT_SUCCESS);
}int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);pid_t cpid;
for(;;){//for循环fork多进程bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));cpid=fork();
if(cpid==-1){
perror("[ERROR] fork():");
close(accept_fd);
}
else if(cpid==0){
do_client(accept_fd);}}
close(accept_fd);
close(sockfd);
return 0;
}
TCP并发服务器多线程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<wait.h>
#include<pthread.h>#define LISTEN_SZ 10void *do_client(void *arg){
char buffer[1024]={0};
ssize_t sbytes,rbytes;
int cfd=*(int *)arg;
memset(buffer,0,sizeof(buffer));
rbytes=recv(cfd,buffer,sizeof(buffer),0);
if(rbytes==-1){
perror("recv():");
pthread_exit(NULL);
}else if(rbytes==0){printf("the client is offline:\n");
pthread_exit(NULL);
}
else if(rbytes>0){sbytes=send(cfd,buffer,sizeof(buffer),0);}
pthread_exit(NULL);
}int main(int argc,const char *argv[]){int sockfd,ret,accept_fd;
struct sockaddr_in svr_addr;
struct sockaddr_in cli_addr;sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1){
perror("[ERROR] socket():");
exit(EXIT_FAILURE);
}bzero(&svr_addr,sizeof(struct sockaddr_in));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(atoi(argv[2]));
svr_addr.sin_addr.s_addr=inet_addr(argv[1]);
ret=bind(sockfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr_in));if(ret==-1){
perror("[ERROR] bind():");
exit(EXIT_FAILURE);
}
ret=listen(sockfd,LISTEN_SZ);
if(ret==-1){
perror("[ERROR] listen():");
close(sockfd);
exit(EXIT_FAILURE);
}
socklen_t len=sizeof(struct sockaddr_in);
int err;
pthread_t tid;
for(;;){bzero(&cli_addr,sizeof(struct sockaddr));accept_fd =accept(sockfd,(struct sockaddr *)&cli_addr,&len);
printf("ip :%s,port:%d\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));
//创建多线程
err=pthread_create(&tid,NULL,do_client,(void *)&accept_fd);
pthread_detach(tid);}
close(accept_fd);
close(sockfd);
return 0;
}
文件上传工具
文件上传工具主要分为如下几个模块
tcp socket 模块 : 主要封装了 tcp socket 的操作
file_transfer 模块 : 实现 文件传输相关操作
debug 模块: 用于打印格式化的调试信息
TcpSocket的封装
debug.h 打印日志信息
#ifndef _DEBUG_H_
#define _DEBUG_H_#define DEBUG_INFO(args...) do{ \
char b__[1024];\
sprintf(b__,args);\
fprintf(stderr,"[%s,%s,%d] : %s",__FILE__,__FUNCTION__,__LINE__,b__);\
}while (0)#endif
tcpsockert.h
#ifndef __TCP_SOCKET_H_
#define __TCP_SOCKET_H_
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
//创建服务端连接
extern int create_tcp_server_socket(const char *ip,unsigned short port);//创建客户端连接
extern int create_tcp_client_socket(const char *svr_ip,const unsigned short svr_port);extern int wait_for_connect(int sfd,struct sockaddr_in *cli_addr);extern void show_tcp_network_address(struct sockaddr_in *sockaddr);
extern ssize_t tcp_send_pack(int sockfd,const void *buf,size_t len);
extern ssize_t tcp_recv_pack(int sockfd,void *buf,size_t len );
#endif
tcpsocket.c
#include"tcpsocket.h"
#include"debug.h"
#define BACKLOG 10int create_tcp_server_socket(const char *ip,unsigned short port){
int ret;
int sfd;struct sockaddr_in svr_addr;
sfd=socket(AF_INET,SOCK_STREAM,0);if(sfd==-1){
DEBUG_INFO("[ERROR] :%s\n",strerror(errno));
return -1;
}
bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(port);
svr_addr.sin_addr.s_addr=inet_addr(ip);
ret=bind(sfd,(const struct sockaddr *)&svr_addr,sizeof(svr_addr));
if(ret==-1){
DEBUG_INFO("[ERROR]:%s",strerror(errno));
return -1;}ret=listen(sfd,BACKLOG);
if(ret==-1){DEBUG_INFO("[ERROR]:%s\n",strerror(errno));
return -1;
}return sfd;
}int create_tcp_client_socket(const char *svr_ip,const unsigned short svr_port){int ret;
int sfd;
struct sockaddr_in svr_addr;sfd=socket(AF_INET,SOCK_STREAM,0);
if(sfd==-1){DEBUG_INFO("[ERROR] :%s\n",strerror(errno));
return -1;
}bzero(&svr_addr,sizeof(svr_addr));
svr_addr.sin_family=AF_INET;
svr_addr.sin_port=htons(svr_port);
svr_addr.sin_addr.s_addr=inet_addr(svr_ip);ret=connect(sfd,(const struct sockaddr *)&svr_addr,sizeof(struct sockaddr));
if(ret=-1){
DEBUG_INFO("[ERROR]:%s\n",strerror(errno));
return -1;
}
DEBUG_INFO("[INFO] :Connect %s succeedd.\n",svr_ip);
return sfd;
}int wait_for_connect(int sfd,struct sockaddr_in *cli_addr){
int cfd;
socklen_t len=sizeof(struct sockaddr_in);
cfd=accept(sfd,(struct sockaddr *) cli_addr,&len);return cfd;}void show_tcp_network_address(struct sockaddr_in *sockaddr){printf("ip : %s\n",inet_ntoa(sockaddr->sin_addr)); printf("port : %d\n",ntohs(sockaddr->sin_port));}
ssize_t tcp_send_pack(int sockfd,const void *buf,size_t len){ return send(sockfd,buf,len,0);
}ssize_t tcp_recv_pack(int sockfd,void *buf,size_t len ){ return recv(sockfd,buf,len,0);
}
编写服务端的main函数
#include "tcpsocket.h"
#include<unistd.h>int main(int argc,char *argv[]){int sfd,cfd;struct sockaddr_in cli_addr;sfd=create_tcp_server_socket(argv[1],atoi(argv[2]));
if(sfd==-1) exit(EXIT_FAILURE);
bzero(&cli_addr,sizeof(struct sockaddr_in));
cfd=wait_for_connect(sfd,&cli_addr);
if(cfd==-1) exit(EXIT_FAILURE);show_tcp_network_address(&cli_addr);
close(cfd);
close(sfd);return 0;}
编写客户端的main函数
#include "tcpsocket.h"
#include <unistd.h>int main(int argc,char *argv[]){int cfd;
cfd=create_tcp_client_socket(argv[1],atoi(argv[2]));if(cfd==-1) exit(EXIT_FAILURE);
close(cfd);
return 0;}
文件传输模块设计
在文件数据传输的过程中,设计了相应的协议,用于传输文件大小与文件名,具体的设计如下:
typedef struct file_protocol{
size_t filesize;
char filename[FILENAME_SZ];
}file_protocol_t;
为防止 tcp 底层粘包, 在传输文件之前先接收协议头信息,在根据协议中的文件大小与文件名,来接收文件数据
file_transfer.h
#include"tcpsocket.h"
#include"debug.h"#define FILENAME_SZ 100typedef struct file_protocol{
size_t filesize;
char filename[FILENAME_SZ];
}file_protocol_t;extern int recv_protocol_head(int cfd,file_protocol_t *p_head);
file_transfer.c
#include"file_transfer.h"
//给file_protocol_t赋值
int recv_protocol_head(int cfd,file_protocol_t *p_head){
ssize_t rbytes;
ssize_t total_received=0;
char *buffer=(char *)p_head;
for(;;){rbytes=tcp_recv_pack(cfd,buffer+total_received,sizeof(file_protocol_t)-total_received);
if(rbytes==-1){
DEBUG_INFO("[ERROR]:%s\n",strerror(errno));
return -1;
}
else if(rbytes==0){
DEBUG_INFO("[INFO]: The client has been shut down!\n");
return-1;
}
else if(rbytes>0){
total_received+=rbytes;
if(total_received==sizeof(file_protocol_t)){break;
}
}if(total_received!=sizeof(file_protocol_t)){
DEBUG_INFO("[ERROR] :Failed to receive data\n");;return -1;
}}return 0;
}
设计接收数据的函数主要功能如下:
- 接收数据
- 将接收的数据写入到文件中
在file_transfer.h中引入文件IO的头文件
#include <unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
在file_transfer.c中
int recv_filedata(int cfd,const char *filename,size_t targetsize){
int fd;
ssize_t rbytes=0,wbytes=0,file_size=0;
char buffer[1024]={0};
DEBUG_INFO("[INFO]:filename %s\n",filename);
fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd==-1){DEBUG_INFO("[ERROR] Failed to open filename");return -1;
}for(;;){
rbytes=tcp_recv_pack(cfd,buffer,sizeof(buffer));
if(rbytes>0){
wbytes=write(fd,buffer,rbytes);
if(wbytes!=rbytes){
DEBUG_INFO("[ERROR]: Failed to write data\n");
return -1;
}
file_size+=rbytes;
if(file_size==targetsize) break;
}
}
close(fd);
return file_size;}
设计文件上传接口的基本逻辑为 :
接收文件头,获取文件大小与文件名
接收数据, 并将数据写入到文件中
int client_uplod_file(int cfd){
int ret;
char *filename;
size_t filesize=0,recvsize=0;
file_protocol_t head;
ret=recv_protocol_head(cfd,&head);
filename=head.filename;
filesize=head.filesize;
recvsize=recv_filedata(cfd,filename,filesize);
return recvsize;
}
在发送文件之前,需要向服务器发送文件名与文件大小(文件头):
int sed_protocol_head(const char *filename,int sockfd){
int fd;
int filesize;
file_protocol_t head;
fd=open(filename,O_RDONLY);
if(fd==-1){DEBUG_INFO("[ERROR] failed to open filename\n");
return -1;
}filesize=lseek(fd,0,SEEK_END);
close(fd);
head.filesize=filesize;
strcpy(head.filename,filename);
ret=tcp_send_pack(sockfd,&head,sizeof(head));
if(ret!=sizeof(file_protocol_t)){
DEBUG_INFO("[ERROR] Failed to send protocol head\n");
return -1;
}return filesize;
}
文件上传发送接口主要用于客户端发送文件数据给服务器,具体设计如下:
边读文件内容边发送数据
int upload_file(const char *filename,int sockfd){int file_size,upload_size;
int fd;
ssize_t rbytes=0,sbytes=0;
char buffer[1024]={0};
file_size=send_protocol_head(filename,sockfd);
if(file_size<0){
return -1;
}
fd=open(filename,O_RDONLY);
if(fd==-1){
DEBUG_INFO("[ERROR] Failed to open filename\n");
return -1;
}
for(;;){
rbytes=read(fd,buffer,sizeof(buffer));
if(rbytes==-1){DEBUG_INFO("[ERROR] Failed to read data\n");return -1;
}
else if(rbytes==0){break;
}//发送数据给服务端
sbytes=tcp_send_pack(sockfd,buffer,rbytes);
if(sbytes==-1){
DEBUG_INFO("[ERROR] Failed to read data\n");
return -1;
}upload_size+=rbytes;}close(fd);
return upload_size;}
服务端代码
#include "tcpsocket.h"
#include<unistd.h>
#include"debug.h"
#include"file_transfer.h"
#include<pthread.h>void *do_task(void *arg){
size_t size;
int cfd=*(int *)arg;
size=client_upload_file(cfd);
printf("client upload file size :%ld\n",size);
pthread_exit(NULL);
}int main(int argc,char *argv[]){int sfd,cfd;
int ret;
pthread_t tid;
struct sockaddr_in cli_addr;sfd=create_tcp_server_socket(argv[1],atoi(argv[2]));
if(sfd==-1) exit(EXIT_FAILURE);
bzero(&cli_addr,sizeof(struct sockaddr_in));for(;;){
cfd=wait_for_connect(sfd,&cli_addr);
if(cfd==-1) exit(EXIT_FAILURE);
show_tcp_network_address(&cli_addr);
ret=pthread_create(&tid,NULL,do_task,(void *)&cfd);
if(ret!=0){DEBUG_INFO("[ERROR] pthread_create():%s\n",strerror(errno));
}
pthread_detach(tid);}
close(cfd);
close(sfd);return 0;}
客户端代码
#include "tcpsocket.h"
#include <unistd.h>
#include"file_transfer.h"int main(int argc,char *argv[]){
if(argc!=4){
fprintf(stderr,"Usage :%s<ip> <port><pathname>\n",argv[0]);
exit(EXIT_FAILURE);
}
int cfd;
cfd=create_tcp_client_socket(argv[1],atoi(argv[2]));
if(cfd==-1) exit(EXIT_FAILURE);upload_file(argv[3],cfd);close(cfd);
return 0;}