网络编程大杂烩
- 网络编程概念
- 1.字节序
- 2.字节序转换
- 3.网络字节序---主机字节序
- 4.地址转换函数
- UDP编程
- 1.概念
- UDP编程核心代码介绍
- 发送数据—sendto 函数
- bind函数
- recvform函数介绍
- 2.UDP简单客户端代码演示
- UDP简单服务器代码演示
- UDP广播功能的实现
- UDP多播功能的实现
- TFTP协议介绍
- 1.1TFTP 概述
- .1.2 TFTP 通信过程
- 1.3TFTP 协议分析
- 1.4客户端文件下载简单流程
- 1.5客户端上传文件简单流程
- 1.6带选项的TFTP文件
- TFTP 通信过程总结(带选项)
- TFTP 的下载过程的代码演示(不带选项)
- TFTP 的上传过程的代码演示(不带选项)
- TFTP 的下载过程的代码演示(带选项)
- TFTP 的上传过程的代码演示(带选项)
- TCP 网络编程
- socket函数(客户端和服务端必用)
- bind函数(服务端专属)
- listen函数(服务端专属)
- accept 函数(服务端专属)
- connect 函数(客户端专属)
- send函数(客户端服务端可用)
- recv函数(客户端服务端可用)
- close 关闭套接字
- 端口复用
- TCP简单客户端的编写
- TCP简单服务器的编写(只能同时跟一个客户端交流)
- TCP三次握手
- TCP四次挥手
- 高并发服务器的编写(进程版)
- 高并发服务器的编写(线程版)
网络编程概念
网络通信要解决的是不同主机进程间的通信
1、首要问题是网络间进程标识问题
2、以及多重协议的识别问题,其网络程 序编程开发接口为 socket 随着 UNIX 以及类 UNIX 操作系统的广泛应用, socket 成为最流行的网络程序开发接口
socket 作用
提供不同主机上的进程之间的通信
socket 特点
1、socket 也称“套接字”
2、是一种文件描述符,代表了一个通信管道的一个端点
3、类似对文件的操作一样,可以使用 read、write、close 等函数对 socket 套接字进行网络数据的收取和发 送等操作
4、得到 socket 套接字(描述符)的方法调用 socket()
1.字节序
概念:是指多字节数据的存储顺序
分类:
1、大端(将高位字节数据存储在低地址)
2:小端(将低位字节数据存储在低地址)
2.字节序转换
主机字节序转网络字节序(网络字节序默认大端,而主机大部分为小端)
uint32_t htonl(uint32_t hostint32);//
uint16_t htons(uint16_t hostint16);
3.网络字节序—主机字节序
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t netint16);
1、网络字节序一直是大端,且主机字节序转网络字节序就是转 大端保存,网络字节序转主机字节序就是大端转大端/小端。
2、只有在多字节数据处理时才需要考虑字节序。
3、同一主机间网络进程通信,不需要字节序转换。
4、不同主机间网络进程通信,需要字节序转换。
4.地址转换函数
点分十进制数转整数
int inet_pton(int family,const char *strptr, void *addrptr);
参数
1、协议族AF_INET
2、点分十进制串
3、转之后的整数
返回值:1成功
将32位无符号整数转换成点分十进制数串
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
参数
1、协议族AF_INET
2、32位无符号整数
3、点分十进制串
4、点分十进制串缓冲区的长度(16)
返回值:成功:则返回字符串的首地址 失败:返回 NULL
UDP编程
1.概念
面向无连接的用户数据报协议,在传输数据前不需要先建立连接;目地主机的运输层收到 UDP 报文后,不需 要给出任何确认
特点:
1、相比 TCP 速度稍快些
2、简单的请求/应答应用程序可以使用 UDP
3、对于海量数据传输不应该使用 UDP
4、广播和多播应用必须使用 UDP
应用:DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
- UDP的通信过程
UDP编程核心代码介绍
发送数据—sendto 函数
发送数据—sendto 函数
ssize_t sendto(int sockfd,const void *buf,
size_t nbytes,int flags,
const struct sockaddr *to,
socklen_t addrlen);
功能: 向 to 结构体指针中指定的 ip,发送 UDP 数据
参数:
sockfd:套接字
buf: 发送数据缓冲区
nbytes: 发送数据缓冲区的大小
flags:一般为 0
to:指向目的主机地址结构体的指针
addrlen:to 所指向内容的长度
注意:
通过 to 和 addrlen 确定目的地址
可以发送 0 长度的 UDP 数据包
返回值:
bind函数
UDP 网络程序想要收取数据需什么条件?
确定的 ip 地址
确定的 port
怎样完成上面的条件呢?
接收端 使用 bind 函数,来完成地址结构与 socket 套接字的绑定,这样 ip、port 就固定了
发送端 在 sendto 函数中指定接收端的 ip、port,就可以发送数据了
int bind(int sockfd,
const struct sockaddr *myaddr,socklen_t addrlen);
功能: 将本地协议地址与 sockfd 绑定
参数:
sockfd: socket 套接字
myaddr: 指向特定协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回 0
失败:其他
recvform函数介绍
接收数据—recvfrom 函数
ssize_t recvfrom(int sockfd, void *buf,
size_t nbytes,int flags,
struct sockaddr *from,
socklen_t *addrlen);
功能:
接收 UDP 数据,并将源地址信息保存在 from 指向的结构中
参数:
sockfd: 套接字
buf:接收数据缓冲区
nbytes:接收数据缓冲区的大小
flags: 套接字标志(常为 0)
from: 源地址结构体指针,用来保存数据的来源
addrlen: from 所指内容的长度
注意:
通过 from 和 addrlen 参数存放数据来源信息
from 和 addrlen 可以为 NULL, 表示不保存数据来源
返回值:
成功:接收到的字符数
失败: -1
2.UDP简单客户端代码演示
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc ,char **argv)
{
//创建一个套接字,本质上是一个文件描述符
struct sockaddr_in det;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
printf("sockfd = %d\n", sockfd);
//指定发送给谁,绑定目的主机
det.sin_family = AF_INET;
det.sin_port = htons(8888);
inet_pton(AF_INET, argv[1],&det.sin_addr);
memset(det.sin_zero, 0, 8);
printf("%u,%u\n", det.sin_addr, det.sin_port);
// int err = bind(sockfd, (struct sockadd *)&det, sizeof(det));
// if(err < 0)
// {
// printf("bind error\n");
// }
while(1)
{
int fd = fork();
if(fd == 0)
{
char buf[50];
socklen_t form_S = sizeof(det);
recvfrom(sockfd, buf, sizeof(buf),0,(struct sockaddr*)&det,&form_S);
printf("from server:%s\n",buf);
}
else
{
char *buf = (char *)malloc(20);
fgets(buf, 20, stdin);
*(buf + strlen(buf) - 1) = '\0';
int a = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&det, sizeof(det));
printf("send num:%d\n", a);
if (strcmp(buf, "bye") == 0)
{
break;
}
}
}
}
UDP简单服务器代码演示
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc ,char **argv)
{
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
int pipe_fd[2];
pipe(pipe_fd);
struct sockaddr_in my_addr;
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(10086);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int err = bind(sock_fd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if(err == -1)
{
printf("bind error\n");
}
while(1)
{
int fd = fork();
if(fd ==0)
{
char buf[20];
unsigned int client_ip;
char from_ip[16]=" ";
struct sockaddr_in client_addr;
socklen_t addrlen=sizeof(client_addr);
client_addr.sin_family = AF_INET;
client_addr.sin_port = htons(10086);
read(pipe_fd[0], (void *)&client_addr.sin_addr, sizeof( client_addr.sin_addr));
// inet_pton(AF_INET, "10.36.145.41",&client_addr.sin_addr);
printf("recevie :%s\n",from_ip);
while(1)
{
memset(buf, 0, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
int err = sendto(sock_fd, buf, strlen(buf), 0,(struct sockaddr *)&client_addr, addrlen);
printf("发送的数据%d\n", err);
}
}
else
{
char buf[20];
char from_ip[16]=" ";
struct sockaddr_in from_addr;
bzero(&from_addr,sizeof(from_addr));
memset(buf, 0, sizeof(buf));
memset(from_ip, 0, sizeof(from_ip));
socklen_t addrlen=sizeof(from_addr);
//接收到源ip
recvfrom(sock_fd, buf, 20, 0, (struct sockaddr *)&from_addr,&addrlen);
write(pipe_fd[1], (void *)&from_addr.sin_addr.s_addr, sizeof(from_addr.sin_addr.s_addr));
//解析源ip的地址
inet_ntop(AF_INET, (const void *)&from_addr.sin_addr.s_addr, from_ip,(socklen_t)16);
printf("receive from :%s\n", from_ip);
printf("buf :%s\n", buf);
// sendto(sock_fd, buf, sizeof(buf), 0, (struct sockaddr *)&from_addr, addrlen);
}
}
}
UDP广播功能的实现
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
/*这是一个广播的程序,如何向局域网进行全体主机的广播,仅限于用在局域网
,城域网和广域网不适用,因为他们太多主机ip了
*/
#if 0
int main(int argc,char**argv)
{
struct sockaddr_in det;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
printf("sockfd = %d\n", sockfd);
//更改mac地址为全f,修改套接字允许发送广播的消息
int yes = 1;
setsockopt(sockfd, SOL_SOCKET,SO_BROADCAST, &yes, sizeof(yes));
//指定广播
det.sin_family = AF_INET;
det.sin_port = htons(8080);
socklen_t addrlen = sizeof(det);
inet_pton(AF_INET, argv[1],&det.sin_addr);
memset(det.sin_zero, 0, 8);
printf("%u,%u\n", det.sin_addr.s_addr, det.sin_port);
while(1)
{
char buf[64] = " ";
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf,sizeof(buf), 0, (const struct sockaddr *)&det, addrlen);
}
}
#endif
UDP多播功能的实现
/*
这是一个udp多播的最基本的演示的程序
*/
int main(int argc,char**argv)
{
struct sockaddr_in det;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
printf("sockfd = %d\n", sockfd);
char group[INET_ADDRSTRLEN] = "224.0.1.1";
//定义一个多播地址
struct ip_mreq mrep;
//添加一个多播组IP
mrep.imr_multiaddr.s_addr = inet_addr(group);
//添加一个将要添加到多播组的ip
mrep.imr_interface.s_addr = htons(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mrep, sizeof(mrep));
//赋值
det.sin_family = AF_INET;
det.sin_port = htons(8080);
/*转换过来就是0.0.0.0,泛指本机的意思
,也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,
这个就表示所有网卡ip地址的意思。
比如一台电脑有3块网卡,分别连接三个网络,
那么这台电脑就有3个ip地址了,如果某个应用程序需要监听某个端口,那他要监听哪个网卡地址的端口呢?*/
det.sin_addr.s_addr = htons(INADDR_ANY);//添加自己本地的ip地址
socklen_t addrlen = sizeof(det);
memset(det.sin_zero, 0, 8);
//绑定
bind(sockfd, (const struct sockaddr *)&det, addrlen);
while(1)
{
char buf[64] = "";
recvfrom(sockfd, buf, sizeof(buf), 0, NULL,NULL);
printf("rece:%s\n", buf);
}
}
/*
多播实验总结
1.要先把当前ip加入到多播组的地址,然后客户端发送消息的时候会发送给 224.0.1.1,这个时候
会把消息发送给所有加入到多播组ip。如果不想了还可以把ip地址移出多播组
*/
TFTP协议介绍
1.1TFTP 概述
TFTP:简单文件传送协议
最初用于引导无盘系统,被设计用来传输小文件
特点:
基于 UDP 协议实现
不进行用户有效性认证
数据传输模式:
octet:二进制模式
netascii:文本模式
mail:已经不再支持
.1.2 TFTP 通信过程
- .服务器从固定端口69接收客户端得读写请求
- 接收到请求后服务器使用一个临时端口发送数据包给客户端,固定为512个字节,也可以改固定字节的长度
- 客户端接收到数据后发送一个ack说我已经收到了
- 如果服务器发送的数据包小于512个字节,那么代表服务器发送数据已经发完了,结束传输
1.3TFTP 协议分析
1.4客户端文件下载简单流程
- 创建udp套接字
- 组读请求的数据包(0,1,“文件名”,0,“模式”,0),发送给服务器
- 循环接收数据,数据包buf长度<516,退出,否则继续
- 查看数据包buf[1],如果buf[1] == 3 ;写文件,回复ack;buf[1] == 5;break;
1.5客户端上传文件简单流程
- 创建套接字
- 组写请求的数据包(0,1,“文件名”,0,“模式”,0)
- 客户端打开要上传的文件
- 循环发送数据buf,如果读本地文件长度<512,break;
1.6带选项的TFTP文件
tsize 选项
当读操作时,tsize 选项的参数必须为“0”,服务器会返回待读取的文件的大小
当写操作时,tsize 选项参数应为待写入文件的大小,服务器会回显该选项
blksize 选项
修改传输文件时使用的数据块的大小(范围:8~65464)
timeout 选项
修改默认的数据传输超时时间(单位:秒)
TFTP 通信过程总结(带选项)
1、可以通过发送带选项的读/写请求发送给 server
2、如果 server 允许修改选项则发送选项修改确认包
3、server 发送的数据、选项修改确认包都是临时 port
4、server 通过 timeout 来对丢失数据包的重新发送
TFTP 的下载过程的代码演示(不带选项)
我们来演示一下如何从服务器下载自己想要的文件存进里面
实现的是TFTP的客户端下载与上传功能,
TFTP软件下载链接
我们要和这个软件配套做实验
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char *argv[]){
//创建套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
printf("创建失败\n");
return 0;
}
else{
printf("创建成功 %d\n",sockfd);
}
//给服务器发送下载请求
struct sockaddr_in ser_addr;
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(69);
inet_pton(AF_INET,"10.36.145.220",(void *)&ser_addr.sin_addr);
char buf[64]="";
int buf_len=sprintf(buf,"%c%c%s%c%s%c",0,1,"a.txt",0,"netascii",0);
sendto(sockfd,buf,buf_len,0,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
//打开本地文件用来接收服务器数据
int fd = open("a.txt",O_RDWR|O_CREAT,0666);
if(fd<0){
perror("open");
return 0;
}
//接收数据包
char rcv_buf[1024]="";
int rcv_buf_len=0;
struct sockaddr_in from_addr;
socklen_t addrlen=sizeof(from_addr);
while(1){
//接收数据函数
bzero(&from_addr,sizeof(from_addr));
rcv_buf_len= recvfrom(sockfd, rcv_buf, sizeof(rcv_buf),0,(struct sockaddr*)&from_addr,&addrlen);
if(rcv_buf[1]==5)//差错
{
printf("error:%d-%d%s\n",rcv_buf[2],rcv_buf[3],rcv_buf+4);
break;
}
else if(rcv_buf[1]==3)//数据包
{
//写文件
write(fd,rcv_buf+4,rcv_buf_len-4);
//回复ACK
rcv_buf[1]=4;
sendto(sockfd,rcv_buf,4,0,(struct sockaddr *)&from_addr,sizeof(from_addr));
if(rcv_buf_len<516)
break;
}
else;
}
close(sockfd);
close(fd);
return 0;
}
TFTP 的上传过程的代码演示(不带选项)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char *argv[]){
//创建套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
printf("创建失败\n");
return 0;
}
else{
printf("创建成功 %d\n",sockfd);
}
//给服务器发送上传请求
struct sockaddr_in ser_addr;
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(69);
inet_pton(AF_INET,"10.36.145.220",(void *)&ser_addr.sin_addr);
char buf[64]="";
int buf_len=sprintf(buf,"%c%c%s%c%s%c",0,2,"a.txt",0,"netascii",0);
sendto(sockfd,buf,buf_len,0,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
//打开本地文件
int fd = open("a.txt",O_RDWR);
if(fd<0){
perror("open");
return 0;
}
//接收数据包
char rcv_buf[1024]="";
int rcv_buf_len=0;
unsigned short p_num=0;
struct sockaddr_in from_addr;//用来保存服务器ip和服务器的临时端口
socklen_t addrlen=sizeof(from_addr);
while(1){
//接收数据函数
bzero(&from_addr,sizeof(from_addr));
recvfrom(sockfd, rcv_buf, sizeof(rcv_buf),0,(struct sockaddr*)&from_addr,&addrlen);
if(rcv_buf[1]==5)//差错
{
printf("error:%s\n",rcv_buf+4);
break;
}
else if(rcv_buf[1]==4)//ACK 0 4 0 0-------0 3 0 1 512byte
{
//读本地文件
rcv_buf_len= read(fd,rcv_buf+4,512);// 0 4 0 0-------0 4 0 0 512byte
rcv_buf[1]=3;// 0 4 0 0-------0 3 0 0 512byte
p_num=ntohs(*(unsigned short *)(rcv_buf+2));//取rcv_buf的3~4字节并保存到主机
*(unsigned short *)(rcv_buf+2)=htons(p_num+1);
//将ack包的编号+1,赋值给数据包0 4 0 x-------0 3 0 x+1 512byte
printf("即将发送数据包编号:%d\n",p_num+1);
sendto(sockfd,rcv_buf,rcv_buf_len+4,0,(struct sockaddr *)&from_addr,sizeof(from_addr));
if(rcv_buf_len<512)
break;
}
else;
}
close(sockfd);
close(fd);
return 0;
}
TFTP 的下载过程的代码演示(带选项)
带选项的好处是可以修改每次发送的数据包的大小,ack的超时时间或者是其他的信息。
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char *argv[]){
//创建套接字
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0){
printf("创建失败\n");
return 0;
}
else{
printf("创建成功 %d\n",sockfd);
}
//给服务器发送下载请求
struct sockaddr_in ser_addr;
bzero(&ser_addr,sizeof(ser_addr));
ser_addr.sin_family=AF_INET;
ser_addr.sin_port=htons(69);
inet_pton(AF_INET,"10.36.145.220",(void *)&ser_addr.sin_addr);
char buf[64]="";
int buf_len=sprintf(buf,"%c%c%s%c%s%c%s%c%s%c",0,1,"a.txt",0,"netascii",0,"blksize",0,"1024",0);//发送带选项请求
sendto(sockfd,buf,buf_len,0,(struct sockaddr *)&ser_addr,sizeof(ser_addr));
//打开本地文件用来接收服务器数据
int fd = open("a.txt",O_RDWR|O_CREAT,0666);
if(fd<0){
perror("open");
return 0;
}
//接收数据包
char rcv_buf[1056]="";
int rcv_buf_len=0;
int len=0;
unsigned short p_num=0;
struct sockaddr_in from_addr;
socklen_t addrlen=sizeof(from_addr);
//接受OACK
recvfrom(sockfd, rcv_buf, sizeof(rcv_buf),0,(struct sockaddr*)&from_addr,&addrlen);
if(rcv_buf[1]==6)//0 6 选项 0 值 0
{
rcv_buf[1]=4;//0 4 选项 0 值 0
rcv_buf[2]=0;//0 4 0 0 值 0
rcv_buf[3]=0;//0 4 0 0 值 0
sendto(sockfd,rcv_buf,4,0,(struct sockaddr *)&from_addr,sizeof(from_addr));
}
else{
return 0;
}
while(1){
//接收数据函数
bzero(&from_addr,sizeof(from_addr));
rcv_buf_len= recvfrom(sockfd, rcv_buf, sizeof(rcv_buf),0,(struct sockaddr*)&from_addr,&addrlen);
if(rcv_buf[1]==5)//差错
{
printf("error:%s\n",rcv_buf+4);
break;
}
else if(rcv_buf[1]==3)//数据包
{
//写文件
len=write(fd,rcv_buf+4,rcv_buf_len-4);
//打印数据包编号
p_num=ntohs(*(unsigned short * )(rcv_buf+2));
printf("数据包编号:%d --- %d\n",p_num,len);
//回复ACK
rcv_buf[1]=4;
sendto(sockfd,rcv_buf,4,0,(struct sockaddr *)&from_addr,sizeof(from_addr));
if(rcv_buf_len<1028)
break;
}
else;
}
close(sockfd);
close(fd);
return 0;
}
TFTP 的上传过程的代码演示(带选项)
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
//客户端有选项的上传
#if 1
int main(int argc,char**argv)
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
int p_num;
char buf[64] ;
char rece_buf[1056] =" ";
printf("sockfd = %d\n", sockfd);
struct sockaddr_in det;
struct sockaddr_in server;
//判断一下 执行的时候是否携带了参数
if(argc<2)
{
printf("你还没有指定服务器的ip地址\n");
return 0 ;
}
//指定发送给谁,绑定目的主机
bzero(&det,sizeof(det));
det.sin_family = AF_INET;
det.sin_port = htons(69);
inet_pton(AF_INET, argv[1],&det.sin_addr);
memset(det.sin_zero, 0, 8);
printf("%u,%u\n", det.sin_addr.s_addr, det.sin_port);
socklen_t addrlen = sizeof(det);
//创建一个接收从服务器下载的文件
//组装数据包并进行发送
int buflen = sprintf(buf,"%c%c%s%c%s%c%s%c%s%c",0,2,"up.txt",0,"netascii",0,"blksize",0,"1024",0);//数据包的大小要对,不能传多了
int sendnum = sendto(sockfd, buf,buflen, 0, (const struct sockaddr *)&det, addrlen);
printf("send num %d \n",sendnum);
int fd = open("a.txt", O_RDWR | O_CREAT, 0666);
if(fd<1)
{
printf("open error \n");
return 0 ;
}
//等待服务器osak的应答
char oask_buf[1056]="";
int rece_osk_num = recvfrom(sockfd, oask_buf, sizeof(oask_buf), 0, ( struct sockaddr *)&server, &addrlen);
if(oask_buf[1]!=6)
{
printf("应答失败\n");
return 0;
}
while(1)
{
//读取数据
int recebuf_len = read(fd, rece_buf + 4, 1028);
//把操作码变成3,表示向服务器发送应答命令 0 0 0 0
rece_buf[1] = 3;
p_num = ntohs(*(unsigned short *)(rece_buf + 2));
*(unsigned short *)(rece_buf + 2) = htons(p_num + 1);
printf("即将发送的编号:%d\n", p_num + 1);
sendto(sockfd, rece_buf,recebuf_len+4, 0,(const struct sockaddr *)&server, addrlen);//发送的时候要使用目的ip和源端口,因为使用的是临时端口,所以要小心
//判断接收的字节是否小于1028,如果小于则结束
if(recebuf_len<1028)
{
printf("文件传输完成\n");
break;
}
// memset(oask_buf, 0, sizeof(oask_buf));
// recvfrom(sockfd, oask_buf, sizeof(oask_buf), 0, (struct sockaddr *)&server, &addrlen);
// if (oask_buf[1] == 5)
// {
// printf("error :%s\n", oask_buf + 4);
// }
// else if(oask_buf[1] == 4)
// {
// printf("服务器应答\n");
// }
}
close(sockfd);
close(fd);
}
#endif
TCP 网络编程
TCP特点:
1、面向连接的流式协议;可靠、出错重传、且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接
TCP客户端与服务端流程
socket函数(客户端和服务端必用)
iint socket(int family,int type,int protocol);
功能:创建一个用于网络通信的 socket 套接字(描述符)
参数:
family:协议族(AF_INET、AF_INET6、PF_PACKET 等),通常使用AF_INET,针对internet的
type:套接字类(SOCK_STREAM、SOCK_DGRAM、SOCK_RAW 等)
SOCK_STREAM:代表TCP协议
SOCK_DGRAM :表示UDP协议
protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP 等 ,通常使用0来代替
返回值:成功返回文件描述符,失败返回-1
特点:创建套接字时,系统不会分配端口 创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要修改为被动的
头文件:#include <sys/socket.h>
bind函数(服务端专属)
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
功能:从函数用于将地址绑定到一个套接字。
参数:
sockfd是由socket函数调用返回的文件描述符。
my_addr是一个指向sockaddr的指针。
addrlen是sockaddr结构的长度。
返回值:失败返回-1
sockaddr的定义:
当调用编程接口函数,且该函数需要传入地址结构时需要用 struct sockaddr 进行强制转换
struct sockaddr{
unisgned short as_family;
char sa_data[14];
};
sockaddr_in的定义:
在定义源地址和目的地址结构的时候,选用 struct sockaddr_in;
struct sockaddr_in{
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
}
参数:
Internet所以sin_family一般为AF_INET。
sin_port:监听的端口号
sin_addr:通常设置为INADDR_ANY表示可以和任何的主机通信
sin_zero[8]:通常不理它
listen函数(服务端专属)
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:将套接字由主动修改为被动 使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
参数:
sockfd: socket 监听套接字
backlog:连接队列的长度
返回值:成功返回 0 失败返回-1
accept 函数(服务端专属)
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
功能:从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
参数:
sockfd: socket 监听套接字
cliaddr: 用于存放客户端套接字地址结构,用来给客户端程序自动填写,服务器端只用传递指针就行。
addrlen:套接字地址结构体长度的地址
返回值:已连接套接字
头文件:#include <sys/socket.h>
注意:返回的是一个已连接套接字,这个套接字代表当前这个连接
connect 函数(客户端专属)
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
功能:主动跟服务器建立链接
参数:
sockfd:socket 套接字
addr: 连接的服务器地址结构
len: 地址结构体长度
返回值:成功:0 失败:其他
注意:
1、connect 建立连接之后不会产生新的套接字
2、连接成功后才可以开始传输 TCP 数据
3、头文件:#include <sys/socket.h>
send函数(客户端服务端可用)
ssize_t send(int sockfd, const void* buf,size_t nbytes, int flags);
功能:用于发送数据
参数:
sockfd: 已建立连接的套接字
buf: 发送数据的地址
nbytes: 发送缓数据的大小(以字节为单位)
flags: 套接字标志(常为 0)
返回值:成功发送的字节数
头文件:#include <sys/socket.h> 注意:不能用 TCP 协议发送 0 长度的数据包
recv函数(客户端服务端可用)
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd 指定接收端套接字描述符;
参数:
buf 指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
len 指明buf的长度;
flags 一般置0。
客户或者服务器应用程序都用recv函数从TCP连接的另一端接收数据。
close 关闭套接字
1、使用 close 函数即可关闭套接字 关闭一个代表已连接套接字将导致另一端接收到一个 0 长度的数据包
2、做服务器时关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接,关闭 accept 返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
3、做客户端时 关闭连接就是关闭连接,不意味着其他
端口复用
- 当出现连接失败,或者地址被占用等错误,可以设置端口复用
- 一个程序多个任务绑定同一个端口
- 一个任务绑定多个端口
- UDP多播也可以设置端口复用
//设置端口复用
int yes=1;
setsockopt(sockfd, SOL_SOCKET,SO_REUSEADDR,&yes, sizeof(yes));
//设置套接字支持广播
int yes=1;
setsockopt(sockfd, SOL_SOCKET,SO_BROADCAST,&yes, sizeof(yes));
//设置多播
setsockopt(sockfd, IPPROTO_IP,IP_ADD_MEMBERSHIP, &merq, sizeof(merq));
TCP简单客户端的编写
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <pthread.h>
int main(int argc,char **argv)
{
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in server_addr;
pthread_t pth;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6666);
socklen_t addrlen = sizeof(server_addr);
inet_pton(AF_INET, "10.36.145.33", &server_addr.sin_addr.s_addr);
int err = connect(sock_fd, (struct sockaddr *)&server_addr, addrlen);
// printf("%u\n", INADDR_ANY);
// char ip[48];
// //
// // send(sock_fd, ip, strlen(ip), 0);
// inet_ntop(AF_INET, &server_addr.sin_addr.s_addr, ip, 16);
// printf("%s\n", ip);
if (err != 0)
{
perror("socket");
}
while (1)
{
char buf[63];
fgets(buf, sizeof(63), stdin);
send(sock_fd, buf, strlen(buf), 0);
}
}
TCP简单服务器的编写(只能同时跟一个客户端交流)
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <pthread.h>
#if 1
/*
功能:只能同时和一个客户端通信,除非客户端退出或者发送bye的指令才能连接下一个
*/
#define client_max 10
struct sockaddr_in cli_addr;
int sock_fd;
int sockfd_new;
int state = 1;
struct sockaddr_in my_addr;
socklen_t addrlen;
int main()
{
// 1.创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in my_addr;
pthread_t pth;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(6666);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addrlen = sizeof(my_addr);
//2.服务器绑定
int err=bind(sock_fd, (struct sockaddr *)&my_addr, addrlen);
if(err == -1)
{
perror("socket");
}
//3.监听套接字,由主动变成被动
err = listen(sock_fd, client_max);
if(err == -1)
{
perror("socket");
}
printf("success\n");
while (1)
{
char ip[16] = "";
if(state==1)
{
// bzero(&cli_addr, addrlen);
//4.等待客户端连接,否则阻塞
sockfd_new = accept(sock_fd, (struct sockaddr *)&cli_addr, &addrlen);
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, ip, addrlen);
printf("%d:%s 已连接\n", sockfd_new, ip);
}
state = 2;
char buf[64]="";
recv(sockfd_new, buf, sizeof(buf), 0);
printf("%d\n", sockfd_new);
if(strncmp(buf,"bye",3)== 0||strlen(buf)== 0)
{
printf("对方已经关闭\n");
state = 1;
break;
}
}
return 0;
}
#endif
TCP三次握手
1.源端口号:发送方端口号
2.目的端口号:接收方端口号
3.序列号:本报文段的数据的第一个字节的序号
4.确认序号:期望收到对方下一个报文段的第一个数据字节的序号
5.首部长度(数据偏移):TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。单 位:32位,即以4字节为计算单位。
6.保留:占6位,保留为今后使用,目前应置为0
7.紧急URG: 此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送
8.确认ACK: 仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把ACK 置1
9.推送PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即 就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH 置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快地(即“推送”向前)交 付给接收应用进程,而不再等到整个缓存都填满后再向上交付
10.复位RST: 用于复位相应的TCP连接
11.同步SYN: 仅在三次握手建立TCP连接时有效。当SYN=1而ACK=0时,表明这是一个连接请求报文段, 对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请 求或连接接受报文
12.终止FIN:用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释 放运输连接。
13.窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)
14.校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上12字节的伪头部
15.紧急指针:仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是 普通数据),即指出了紧急数据的末尾在报文中的位置,注意:即使窗口为零时也可发送紧急数据
16.选项:长度可变,最长可达40字节,当没有使用选项时,TCP首部长度是20字节
第一次连接:当客户端调用connect函数时,客户端一个SYN包给服务器给服务器:序列号 =0,确认序号 = 0
第二次连接:服务器接收到syn包时,第一次SYN给客户端:序列号 0 ,确认序号为1
第三次连接:客户端表示收到了并且发一个ack给服务器 :序列号0,确认序号 =1
TCP四次挥手
我们回顾一下TCP报头:
四次挥手的条件:调用close函数
我们由图可知道:
第一次挥手:序列号= m 确认号 = m+1
第二次挥手:序列号=m+1 确认号 = m+1,这个时候回复一个ACK为1
第三次挥手:序列号=m+1 确认号 = m+1,再次发一个FIN的报文
第四次挥手: 序列号 = m+1 确认号 = m+2 ,最后发送一个ACK应答报文
总结:假设是客户端断开了连接,先发送一个FIN,服务器回复一个ACK应答,发送完应答后再次发送一个FIN,最后客户端发送一个ACK确认断开连接。
问题:为什么断开连接和ack不能在一个数据包中发送
答:因为只有调用close函数才会发送断开连接数据包,但是ack只要收到对方断开连接数据包就会回复,ack和fin之间存在时间差,所以不可以同时发送ack和fin,就会产生一端连续发两次给另一端
问题:主动断开连接的是客户端,当收到服务器发送到的ack后,还能在接收数据。
答:因为还没有挥手完,这个时候客户端处于半关闭状态,可以接收数据。
高并发服务器的编写(进程版)
概念:以前咱们编写的是一个服务器只能同时连接一个客户端,高并发的服务器是可以同时连接多个客户端并进行通信。
#if 1
/*
功能:一个服务器同时和多个客户端通信(使用进程的方法)
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <pthread.h>
#define client_max 10
struct sockaddr_in cli_addr;
int sock_fd;
int sockfd_new;
int state = 1;
int pid;
struct sockaddr_in my_addr;
socklen_t addrlen;
void *pthread (void *arg)
{
}
int main()
{
// 1.创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in my_addr;
pthread_t pth;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(6666);//这个端口要和客户端的端口一样
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addrlen = sizeof(my_addr);
//设置端口复用
int yes = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
//2.服务器绑定
int err = bind(sock_fd, (struct sockaddr *)&my_addr, addrlen);
if (err == -1)
{
perror("socket");
}
//3.监听套接字,由主动变成被动
err = listen(sock_fd, client_max);
if (err == -1)
{
perror("socket");
}
printf("success\n");
while (1)
{
// bzero(&cli_addr, addrlen);
//4.等待客户端连接,否则阻塞
sockfd_new = accept(sock_fd, (struct sockaddr *)&cli_addr, &addrlen);
pid = fork();
if (pid < 0)
{
printf("fork error\n");
}
else if (pid == 0)
{
close(sock_fd);//这个套接字用不到
char ip[16] = "";
inet_ntop(AF_INET, &cli_addr.sin_addr.s_addr, ip, addrlen);
printf("已连接%s\n", ip);
while (1)
{
char buf[64] = "";
recv(sockfd_new, buf, sizeof(buf), 0);
printf("%d\n", sockfd_new);
if (strncmp(buf, "bye", 3) == 0 || strlen(buf) == 0)
{
printf("对方已经关闭\n");
close(sockfd_new);
break;
}
}
exit(1);
}
else if (pid > 0)
{
close(sockfd_new);//父进程不需要这个,子进程内才需要
}
}
return 0;
}
#endif
高并发服务器的编写(线程版)
概念:以前咱们编写的是一个服务器只能同时连接一个客户端,高并发的服务器是可以同时连接多个客户端并进行通信。
#if 1
/*
功能:一个服务器同时和多个客户端通信(使用线程的方法)
*/
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <strings.h>
#include <pthread.h>
#define client_max 10
int sock_fd;
int pid;
typedef struct
{
int sockfd_new;
char ip[16];
struct sockaddr_in cli_addr;
} Myadd;
struct sockaddr_in my_addr;
socklen_t addrlen;
void *pthread (void *arg)
{
Myadd p = *(Myadd *)arg;
inet_ntop(AF_INET, &p.cli_addr.sin_addr.s_addr, p.ip, 16);
printf("%d,%s已连接\n",p.sockfd_new, p.ip);
while (1)
{
printf("%p\n", &p);
char buf[64] = "";
recv(p.sockfd_new, buf, sizeof(buf), 0);
printf("%d,%s", p.sockfd_new, buf);
if (strncmp(buf, "bye", 3) == 0 || strlen(buf) == 0)
{
printf("对方已经关闭\n");
close(p.sockfd_new);
break;
}
}
}
int main()
{
// 1.创建套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in my_addr;
pthread_t pth;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(6666);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
addrlen = sizeof(my_addr);
//设置端口复用
int yes = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
//2.服务器绑定
int err = bind(sock_fd, (struct sockaddr *)&my_addr, addrlen);
if (err == -1)
{
perror("socket");
}
//3.监听套接字,由主动变成被动
err = listen(sock_fd, client_max);
if (err == -1)
{
perror("socket");
}
printf("success\n");
while (1)
{
Myadd add;
bzero(&add.cli_addr, addrlen);
//4.等待客户端连接,否则阻塞
add.sockfd_new = accept(sock_fd, (struct sockaddr *)&add.cli_addr, &addrlen);
pthread_create(&pth, NULL, pthread, (void*)&add);//创建线程
pthread_detach(pth);
usleep(1000);
}
return 0;
}
#endif