一、端口号
- 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分
- TCP端口号与UDP端口号独立
- 端口用两个字节来表示 2byte(65535个)
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)`
`已登记端口:1024~49151 (选1000以上10000以下) `
`动态或私有端口:49152~65535`
`
二、字节序
小端序(little-endian) - 低序字节存储在低地址
大端序(big-endian) - 高序字节存储在低地址
网络中传输一字节以上的带类型的数据(比如short、int),必须使用网络字节序,即大端字节序。
查看主机是大端序还是小端序。
网络传输中,需要将每个主机的主机字节序(CPU决定),转换为网络中统一顺序的网络字节序
才能供双方主机去识别
只需要转换IP和port就可以,不需要转换传输的数据包的字节序
因为IP和port为 4个字节和2个字节, 而数据报一般都为char类型, 占一个字节
根据字节序的性质,内存存储大于一个字节类型的数据在内存中的存放顺序 。
所以char类型并不具有字节序的概念
1.主机字节序到网络字节序 (小端序->大端序)
#include <arpa/inet.h>`
`u_long htonl` `(u_long hostlong);` `//host to internet long`
`功能:将无符号整数hostlong从主机字节顺序转换为网络字节顺序。`
`#include <arpa/inet.h>`
`u_short htons` `(u_short short);` `//掌握这个`
`功能:将无符号短整数hostshort从主机字节顺序到网络字节顺序。`
`
2.网络字节序到主机字节序(大端序->小端序)
#include <arpa/inet.h>`
`u_long ntohl` `(u_long hostlong);`
`功能:将无符号整数netlong从网络字节顺序转换为主机字节顺序。`
`#include <arpa/inet.h>`
`u_short ntohs` `(u_short short);//端口 2byte`
`功能:将无符号短整数netshort从网络字节顺序转换为主机字节顺序。的`
`
#include <stdio.h>`
`#include <arpa/inet.h>`
`int main(int argc, char const *argv[])`
`{`
` int a = 0x12345678;`
` //小端转大端`
` int b = htonl(a);`
` printf("%#x\n", b);`
` //大端转小端`
` int c = ntohl(b);`
` printf("%#x\n", c);`
` return 0;`
`}`
`
三、IP地址转换
1、 inet_addr 主机字节序转换为网络字节序
#include<sys/socket.h> `
`#include<netinet/in.h> `
`#include<arpa/inet.h>`
`in_addr_t inet_addr(const char *strptr); //该参数是字符串`
`typedef uint32_t in_addr_t;`
`struct in_addr `
`{`
` in_addr_t s_addr;`
`};`
`功能: 主机字节序转为网络字节序`
`参数: const char *strptr: 字符串`
`返回值: 返回一个无符号长整型数(无符号32位整数用十六进制表示), `
` 否则NULL`
`
2、 inet_ntoa 网络字节序转换为主机字节序
#include <sys/socket.h>`
`#include <netinet/in.h>`
`#include <arpa/inet.h>`
`char` `*inet_ntoa(struct` `in_addr inaddr);`
`功能: 将网络字节序二进制地址转换成主机字节序。 `
`参数:` `struct` `in_addr in addr : 只需传入一个结构体变量`
`返回值: 返回一个字符指针, 否则NULL;`
`
四、TCP编程
C/S B/S
client/server browser/server
客户端/服务器 浏览器/服务器
1、套接字工作流程
客户端: 发送请求
服务器端: 相应请求
服务器:
1.创建流式套接字(socket())------------------------> 有手机
2.指定本地的网络信息(struct sockaddr_in)----------> 有号码
3.绑定套接字(bind())------------------------------>绑定手机
4.监听套接字(listen())---------------------------->待机
5.链接客户端的请求(accept())---------------------->接电话
6.接收/发送数据(recv()/send())-------------------->通话
7.关闭套接字(close())----------------------------->挂机
客户端:
1.创建流式套接字(socket())----------------------->有手机
2.指定服务器的网络信息(struct sockaddr_in)------->有对方号码
3.请求链接服务器(connect())---------------------->打电话
4.发送/接收数据(send()/recv())------------------->通话
5.关闭套接字(close())--------------------------- >挂机
服务器端(server):
-
socket(),创建套接字文件,创建出用于连接的套接字文件
-
bind(), 绑定,把socket()函数返回的文件描述符和IP、端口号进行绑定;
-
listen(), 监听,将socket()返回的文件描述符,由主动套接字变为被动套接字;
-
accept(), 阻塞函数,阻塞等待客户端的连接请求, 返回一个用于通信的套接字文件;
-
recv(), 接收客户端发来的数据;(read)
//6) send(), 发送数据;(write)
- close(), 关闭文件描述符; 至少要关闭: 连接、通信
客户端(client):
- socket(),创建套接字文件,既用于连接,也用于通信;
填充结构体: 填充服务器的ip和端口 , 用于connect连接
-
connect(); 用于发起连接请求,阻塞等待连接服务器;
-
send(), 发送数据;
//4) recv(), 接收数据;
5)close(), 关闭文件描述符;
2、函数接口
1)socket 创建套接字
#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`int` `socket(int domain,` `int type,` `int protocol);`
`功能:创建套接字`
`参数:`
` domain:协议族`
` AF_UNIX, AF_LOCAL 本地通信`
` AF_INET ipv4`
` AF_INET6 ipv6`
` type:套接字类型`
` SOCK_STREAM:流式套接字`
` SOCK_DGRAM:数据报套接字`
` protocol:协议 - 填0 自动匹配底层 ,根据type系统默认自动帮助匹配对应协议`
` 传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP`
` 网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)`
` 返回值:`
` 成功 文件描述符 0` `-> 标准输入 1->标准输出 2->标准出错 `
`3->socket`
` 失败 -1,更新errno`
`
2)bind 绑定 ipv4 ip和端口
#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`int` `bind(int sockfd,` `const` `struct` `sockaddr` `*addr,socklen_t addrlen);`
`功能:绑定 ipv4 ip和端口 `
`参数`
` sockfd:文件描述符`
` addr:通用结构体,根据socket第一个参数选择的通信方式最终确定这需要真正填充传递的结构体是那个类型。强转后传参数。`
` addrlen:填充的结构体的大小 `
`返回值:成功0 失败-1、更新errno`
`通用结构体:相当于预留一个空间`
`struct` `sockaddr`
`{`
`sa_family_t sa_family;`
`char sa_data[14];`
`}`
`ipv4的结构体 `
`struct` `sockaddr_in`
`{`
`sa_family_t sin_family;` `//协议族AF_INET`
`in_port_t sin_port;` `//端口号`
`struct` `in_addr sin_addr;` `//ip地址`
`};`
`struct` `in_addr` `11`
`{`
`uint32_t s_addr;` `//IP地址 `
`};`
` 本地址通信结构体:`
`struct` `sockaddr_un`
`{`
`sa_family_t sun_family;` `//AF_UNIX `
`char sun_path[108];` `//在本地创建的套接字文件的路径及名字`
`};`
`ipv6通信结构体:`
`struct` `sockaddr_in6`
`{`
`sa_family_t sin6_family;`
`in_port_t sin6_port;`
`uint32_t sin6_flowinfo;`
`struct` `in6_addr sin6_addr;`
`uint32_t sin6_scope_id;`
`};`
`struct` `in6_addr`
`{`
`unsigned` `char s6_addr[16];`
`};`
`//如果绑定使用通用地址可使用 INADDR_ANY宏(结合代码讲的时候再添加)`
`含义是自动绑定所有本机网卡的地址,`
`
3)listen 监听,将主动套接字变为被动套接字
#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`int` `listen(int sockfd,` `int backlog);`
`功能:监听,将主动套接字变为被动套接字`
`参数:`
` sockfd:套接字`
` backlog:同时响应客户端请求链接的最大个数,不能写0.`
`不同平台可同时链接的数不同,一般写6-8个`
`返回值:成功 0 失败-1,更新errno `
`
4)accept 阻塞函数
#include <sys/socket.h>`
`int` `accept(int sockfd,` `struct` `sockaddr` `*addr,` `socklen_t` `*addrlen);`
`accept(sockfd,NULL,NULL);`
`阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,`
`则accept()函数返回,返回一个用于通信的套接字文件描述符(4);`
`参数:`
` Sockfd :套接字`
` addr: 链接客户端的ip和端口号`
` 如果不需要关心具体是哪一个客户端,那么可以填NULL;`
` addrlen:结构体的大小`
` 如果不需要关心具体是哪一个客户端,那么可以填NULL;`
`不需要查看链接的客户端ip和端口号`
` accept(sockfd,NULL,NULL);`
`需要查看链接的客户端ip和端口号`
` accept(sockfd,(struct sockaddr *)&saddr,&len);`
` 返回值: `
` 成功:文件描述符;` `//用于通信`
`失败:-1,更新errno`
`同时返回客户端的ip和端口号。保存到结构体struct sockaddr *addr中。`
`通过结构体访问到:inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)`
`打印时记得使用 inet_ntoa()和ntohs()转换为主机字节序`
`
5)recv 接收数据
#include <sys/types.h>`
`#include <sys/socket.h>`
`ssize_t` `recv(int sockfd,` `void` `*buf,` `size_t len,` `int flags);`
`功能: 接收数据 `
`参数: `
` sockfd: acceptfd ;`
` buf 存放位置`
` len 大小`
` flags 一般填0,相当于read()函数`
` MSG_DONTWAIT 非阻塞`
`返回值: `
`<` `0 失败出错 更新errno`
`==0 表示客户端退出`
`>0 成功接收的字节个数`
`
发送端协议类型为TCP Client,远程目标ip地址为服务器IP地址
端口号也应该一致
先关闭客户端,再关闭服务器。
6)connect 用于连接服务器;
#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`int` `connect(int sockfd,` `const` `struct` `sockaddr` `*addr,socklen_t addrlen);`
`功能:用于连接服务器;`
`参数:`
` sockfd:socket函数的返回值`
` addr:填充的结构体是服务器端的;`
` addrlen:结构体的大小`
`返回值 `
`-1 失败,更新errno`
` 正确 0`
`
7)send 发送数据,用来发送消息到一个套接字中。
#include <sys/types.h>`
`#include <sys/socket.h>`
`ssize_t` `send(int sockfd,` `const` `void` `*buf,` `size_t len,` `int flags);`
`功能:发送数据,用来发送消息到一个套接字中。只能在套接字处于连接状态的时候才能使用。`
`参数:`
` sockfd:socket函数的返回值`
` buf:发送内容存放的地址`
` len:发送内存的长度`
` flags:如果填0,相当于write();`
`返回值;`
` 成功 发送的字节数`
` 失败 -1`
`可以使用write代替send, 都属于往发送缓存区内写入数据`
`read代替recv , 都属于往接收缓存区内提取数据`
`
测试注意:
-
如果使用客户端软件进行连接,必须保证windows和虚拟机在同一个局域网(桥接),并能互相ping通。服务器的IP地址必须指定为虚拟机自己的IP。
-
必须保证客户端正常退出后在关闭服务器程序,在客户端连接状态情况下强制关闭服务器程序,下次启动服务器程序后会提示bind err 。这是因为没有正常释放绑定的端口,等1~2分钟就可以了。
3、代码优化
1。端口和ip地址通过命令行传参到代码中。(如果参数不一致,应该提示并退出)
2。服务器端IP地址可由"0.0.0.0"自动获取,或者由宏定义自动获取
3。客户端发送去掉fgets获取的多余的'\n'.
fgets(实际读到的内容小于等于指定个数-1,自动读到的内容后添加'\0',会将'\n'也读入)
if(buf[strlen(buf)-1] == '\n')//去掉fgets获取的'\n'
buf[strlen(buf)-1] ='\0';
4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。
int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);
打印时记得使用 inet_ntoa()和ntohs()转换为主机字节序
printf("client ip:%s ,port:%d\n", inet_ntoa(saddr.sin_addr),ntohs(saddr.sin_port));
5、当客户端输入quit的时候,客户端退出
strncmp
6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
accept处使用while到接受数据结束。
/*服务器创建代码 */`
`#include <stdio.h>`
`#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`#include <netinet/in.h>`
`#include <netinet/ip.h> /* superset of previous */`
`#include <arpa/inet.h>`
`#include <unistd.h>`
`#include <stdlib.h>`
`#include <string.h>`
`int main(int argc, char const *argv[])`
`{`
` if (argc < 2)`
` {`
` printf("plase input <ip><port>\n");`
` return -1;`
` }`
` //1.创建套接字,用于链接`
` int sockfd;`
` sockfd = socket(AF_INET, SOCK_STREAM, 0);`
` if (sockfd < 0)`
` {`
` perror("socket err");`
` return -1;`
` }`
` //文件描述符 0 -> 标准输入 1->标准输出 2->标准出错 3->socket`
` printf("sockfd:%d\n", sockfd);`
` //2.绑定 ip+port 填充结构体`
` struct sockaddr_in saddr;`
` saddr.sin_family = AF_INET; //协议族ipv4`
` saddr.sin_port = htons(atoi(argv[1])); //端口号,htons将无符号短整数hostshort从主机字节顺序到网络字节顺序。`
`//atoi(),字符串转整型`
`//saddr.sin_addr.s_addr = inet_addr(argv[1]);//ip地址,转化为16进制表示`
`#if 0`
` saddr.sin_addr.s_addr = inet_addr("0.0.0.0");//ip地址,转化为16进制表示`
`#else`
` saddr.sin_addr.s_addr = INADDR_ANY;`
`#endif`
` socklen_t len = sizeof(saddr); //结构体大小`
` //bind绑定ip和端口`
` if (bind(sockfd, (struct sockaddr *)&saddr, len) < 0)`
` {`
` perror("bind err");`
` return -1;`
` }`
` printf("bind success\n");`
` //3.启动监听,把主动套接子变为被动套接字`
` if (listen(sockfd, 6) < 0)`
` {`
` perror("listen err");`
` return -1;`
` }`
` printf("listen success\n");`
` //4.阻塞等待客户端的链接请求`
` int acceptfd;`
` // acceptfd = accept(sockfd,NULL,NULL);`
` while (1)`
` {`
` acceptfd = accept(sockfd, (struct sockaddr *)&saddr, &len);`
` //获取客户端的ip和端口,(struct sockaddr *)&saddr:用来存放返回的ip,和端口`
` if (acceptfd < 0)`
` {`
` perror("accept err");`
` return -1;`
` }`
` printf("client ip:%s ,port:%d\n", inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port));`
` printf("connect success\n");`
` //5.接收数据`
` char buf[64];`
` int ret;`
` while (1)`
` {`
` ret = recv(acceptfd, buf, sizeof(buf), 0);`
` if (strncmp(buf, "quit", 4) == 0) //接收到quit退出`
` {`
` break;`
` }`
` if (ret < 0)`
` {`
` perror("recv err.");`
` return -1;`
` }`
` else if (ret == 0) //客户端退出`
` {`
` printf("client exit\n");`
` break;`
` }`
` else`
` {`
` printf("buf:%s\n", buf);`
` }`
` }`
` }`
` close(sockfd);`
` close(acceptfd);`
` return 0;`
`}`
`
/*客户端创建代码 */`
`#include <stdio.h>`
`#include <sys/types.h> /* See NOTES */`
`#include <sys/socket.h>`
`#include <netinet/in.h>`
`#include <netinet/ip.h> /* superset of previous */`
`#include <arpa/inet.h>`
`#include <unistd.h>`
`#include <stdlib.h>`
`#include <string.h>`
`int main(int argc, char const *argv[])`
`{`
` if (argc < 3)`
` {`
` printf("plase input <ip><port>\n");`
` return -1;`
` }`
` //1.创建套接字,用于链接`
` int sockfd;`
` sockfd = socket(AF_INET, SOCK_STREAM, 0);`
` if (sockfd < 0)`
` {`
` perror("socket err");`
` return -1;`
` }`
` //文件描述符 0 -> 标准输入 1->标准输出 2->标准出错 3->socket`
` printf("sockfd:%d\n", sockfd);`
` //2.绑定 ip+port 填充结构体`
` struct sockaddr_in saddr;`
` saddr.sin_family = AF_INET; //协议族ipv4`
` saddr.sin_port = htons(atoi(argv[2])); //端口号,htons将无符号短整数hostshort从主机字节顺序到网络字节顺序。`
` saddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址,转化为16进制表示`
` socklen_t len = sizeof(saddr); //结构体大小`
` //3用于连接服务器;`
` if (connect(sockfd, (struct sockaddr *)&saddr, len) < 0)`
` {`
` perror("connect err");`
` return -1;`
` }`
` //4发送信息`
` char buf[64] = {0};`
` while (1)`
` {`
` fgets(buf, sizeof(buf), stdin); //从终端获取内容存放到数组中`
` if (strncmp(buf, "quit", 4) == 0) //输入quit退出客户端`
` {`
` break;`
` }`
` if (buf[strlen(buf)] == '\0')`
` {`
` buf[strlen(buf) - 1] = '\0';`
` } //将fgets自动补的\n去掉`
` send(sockfd, buf, sizeof(buf), 0); //将数组中的内容发送到主机端`
` }`
` close(sockfd);`
` // close()`
` return 0;`
`}`
`
ftp文件传输协议
tcp实现ftp功能:
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍:
均有服务器和客户端代码,基于TCP写的。
在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。
相当于另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:四个功能
***************list**************//列出服务器所在目录下的文件名(除目录不显示)
***********put filename**********//客户端给服务器上传一个文件
***********get filename**********//客户端从服务器所在路径下载文件
**************quit***************//退出(可只退出客户端,服务器等待下一个客户端链接)
//读文件,读到最后一次,最后再发送end
//粘包:1.延时 2.读多少发多少
服务器
- 搭建tcp框架
- 服务器接收消息
//list 列出服务器所在目录下的文件名 目录操作 文件属性获取--发送给客户端
//put filename //接收文件 recv --》新建文件---》 写文件
//get filename //读文件--》发送给客户端
//quit while(1)accpet;
客户端
- 搭建tcp框架
- 列出功能
- 给服务器发消息
//list 接收--》列出服务器所在目录下的文件名
//put filename 找到文件--》读文件 -》发送
//get filename //接收文件 recv --》新建文件---》 写文件
//quit