1. 网络编程概述(444.1)
TCP/UDP对比
- TCP 面向连接(如打电话要先拨号建立连接);UDP 是无连接的,即发送数据之前不需要建立连接
- TCP 提供可靠的服务。也就是说,通过 TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付
- TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP 是面向报文的 UDP 没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP 电话,实时视频会议等)
- 每一条 TCP 连接只能是点到点的;UDP 支持一对一,一对多,多对一和多对多的交互通信
- TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节
- TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道
端口号作用
- 一台拥有 IP 地址的主机可以提供许多服务,比如 Web 服务、FTP 服务、SMTP 服务等
- 这些服务完全可以通过 1 个 IP 地址来实现。那么,主机是怎样区分不同的网络服务呢?显然不能只靠 IP 地址,因为 IP 地址与网络服务的关系是一对多的关系。
- 实际上是通过 “IP 地址 + 端口号” 来区分不同的服务的。端口提供了一种访问通道,服务器一般都是通过知名端口号来识别的。例如,对于每个 TCP/IP 实现来说,FTP 服务器的 TCP 端口号都是 21,每个 Telnet 服务器的 TCP 端口号都是 23,每个 TFTP (简单文件传送协议)服务器的 UDP 端口号都是 69。
2. 字节序(445.2)
- 字节序,即字节在电脑中存放时的序列与输入(输出)时的序列是先到的在前还是后到的在前。
- 字节序是指多字节数据在计算机内存中存储或者网络传输时各字节的存储顺序。
- Little endian:将低序字节存储在起始地址
- Big endian:将高序字节存储在起始地址
- LE little-endian 小端字节序
- BE big-endian 大端字节序
- 网络字节序 = 大端字节序
字节序转换api
#include <arpa/inet.h>
uint16_t htons(uint16_t host16bitvalue); //返回网络字节序的值
uint32_t htonl(uint32_t host32bitvalue); //返回网络字节序的值
uint16_t ntohs(uint16_t net16bitvalue); //返回主机字节序的值
uint32_t ntohl(uint32_t net32bitvalue); //返回主机字节序的值
- h代表host主机,n代表net,s代表short(两个字节),l代表long(4个字节),通过上面的4个函数可以实现主机字节序和网络字节序之间的转换。有时可以用INADDR_ANY,INADDR_ANY指定地址让操作系统自己获取
3. socket编程步骤(446.3)
Sockt服务器和客户端的开发步骤
- TCP Server
- 创建套接字(socket)
- 为套接字添加信息(IP地址和端口号)(bind)
- 监听网络连接(listen)
- 监听到有客户端接入,接受一个连接(accept)
- 数据交互(read、write、read)
- 关闭套接字,断开连接(close)
4. Linux提供的API简析(447.4)
- 指定讲“汉语”(连接协议)
int socket(int domain, int type, int protocol);
- 地址准备好
- 地址转换API
int inet_aton(const char* straddr,struct in_addr *addrp);
把字符串形式的“192.168.1.123”转为网络能识别的格式char* inet_ntoa(struct in_addr inaddr);
把网络格式的ip地址转为字符串形式
- 监听
- 连接
- 数据收发
5.1 数据收发常用第一套API
5.2 数据收发常用第二套API
- 客户端的connect函数
5. socket服务端代码实现一(448.5)
man 2 socket
、man 2 bind
、man htons
、man inet_aton
查找某段源码在哪个头文件之下的方法
cd /usr/include
ls
grep "struct sockaddr_in {" * -nir
- 搜索其在哪个头文件下被定义/声明
- ※ (星号) 在当前目录底下
- n 找出时显示其所在行号
- i 不区分大小写
- r 递归的方式
vi linux/in.h +261
grep "struct in_addr {" * -nir
vi linux/in.h +89
代码相关
- SOCKET/server.c
#include <stdio.h>//printf
#include <sys/types.h>//socket bind /* See NOTES */
#include <sys/socket.h>//socket bind
//#include <linux/in.h>//struct sockaddr_in//和<netinet/in.h>冲突
#include <arpa/inet.h>//htons
#include <netinet/in.h>//inet_aton
#include <stdlib.h>//exit
//int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*struct sockaddr_in {//也可在<linux/in.h>中找到
__kernel_sa_family_t sin_family; /* Address family */
//__be16 sin_port; /* Port number */
//struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
//unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
// sizeof(unsigned short int) - sizeof(struct in_addr)];
//};*/
/*struct in_addr {
__be32 s_addr;
};*/
//int listen(int sockfd, int backlog);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//fd,客户端地址,客户端地址长度地址,返回新的建立的socket的通道
int main(int argc, char **argv)
{
int s_fd;
//1. socket 获取/创建套接字
s_fd = socket(AF_INET, SOCK_STREAM, 0);//IPv4 因特网域,TCP协议,自动选择type类型对应的默认协议(也可用宏:IPPROTO_TCP TCP)
if(s_fd == -1){//判断建立socket通道是否成功
perror("socket");//把错误的问题打出来
exit(-1);//退出这个程序
}
//2. bind 绑定
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET;//协议族,跟domain一致
s_addr.sin_port = htons(8989);//端口号一般3000以下为操作系统来用,建议用户5000以上 端口号由主机to网络 返回网络字节序的值
inet_aton("192.168.2.13",&s_addr.sin_addr);//本机ip地址由字符串形式to网络格式//ifconfig
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));//把ip地址端口号和s_fd绑定起来//fd,强转为struct sockaddr型,结构体的长度
//3. listen
listen(s_fd,10);//监听10个连接,不阻塞
//4. accept 接受
int c_fd = accept(s_fd,NULL,NULL);//先不关心客户端是谁//如果没连接到,会卡在这//后续的操作用新的c_fd
//5. read
//6. write
printf("connected!\n");
while(1)//有客户端连上,不让其退出
return 0;
}
ifconfig
本机网络地址和本机回环地址:
win10 telnet不是内部或外部命令(已解决)
- telnet 在win10下默认是不开启的,所以需要我们自己手动开启。
- win+s 搜索:控制面板,点击进入;
- 在控制面板中,选择程序——启动或关闭 windows 功能,勾选 Telnet 客户端选项,确定进行安装。
勾选 “Telnet 客户端”
更改 windows 设置需要一分钟左右的时间,不要关闭。安装完成后重启计算机生效。(实测可不重启)
- windows功能的telnet功能已经开启,我们测试下是否可以正常使用,输入telnet+ip地址。
6. socket服务端代码实现二(449.6)
- SOCKET/server2.c(建立服务器并等待客户端连接 后收发)
#include <stdio.h>//printf
#include <sys/types.h>//socket bind /* See NOTES */
#include <sys/socket.h>//socket bind
//#include <linux/in.h>//struct sockaddr_in//和<netinet/in.h>冲突
#include <arpa/inet.h>//htons
#include <netinet/in.h>//inet_aton
#include <stdlib.h>//exit
#include <string.h>//memset
#include <unistd.h>//read write
//int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*struct in_addr {
__be32 s_addr;
};*/
//int listen(int sockfd, int backlog);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//fd,客户端地址,客户端地址长度,返回新的建立的socket的通道
int main()
{
int s_fd;//1. socket
int c_fd;//4. accept
int n_read;//5. read
char readBuf[128];//5. read//定义一个数组就不用像指针还要开辟空间
char *msg = "I get your connection\n";//6. write
struct sockaddr_in s_addr;//2. bind
struct sockaddr_in c_addr;//2. bind
memset(&s_addr,0,sizeof(struct sockaddr_in));//清空
memset(&c_addr,0,sizeof(struct sockaddr_in));//清空
//1. socket 获取/创建套接字
s_fd = socket(AF_INET, SOCK_STREAM, 0);//IPv4 因特网域,TCP协议,自动选择type类型对应的默认协议(也可用宏:IPPROTO_TCP TCP)
if(s_fd == -1){//判断建立socket通道是否成功
perror("socket");//把错误的问题打出来
exit(-1);//退出这个程序
}
//2. bind 绑定
s_addr.sin_family = AF_INET;//协议族,跟domain一致
s_addr.sin_port = htons(8989);//端口号一般3000以下为操作系统来用,建议用户5000以上 端口号由主机to网络 返回网络字节序的值
inet_aton("192.168.2.13",&s_addr.sin_addr);//本机ip地址由字符串形式to网络格式//ifconfig
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));//把ip地址端口号和s_fd绑定起来
//3. listen
listen(s_fd,10);//监听10个连接
//4. accept 接受
int clen = sizeof(struct sockaddr_in);
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);//如果没连接到,会卡在这
if(c_fd == -1){
perror("accept");
}
printf("get connected: %s\n",inet_ntoa(c_addr.sin_addr));//连上后打印 “已连接” 和客户端的 ip地址
//5. read
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("get message:%d,%s\n",n_read,readBuf);//打印收到了多少个字节的消息,和消息的内容
}
//6. write
write(c_fd,msg,strlen(msg));//用提前定义的字符串,如直接写入字符串,字节大小也随意写如128,会造成乱码因有无用的字节
return 0;
}
7. socket客户端代码实现(450.7)
man 2 connect
- SOCKET/client.c(连接服务器后 客户端发收)
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>//read write
//int socket(int domain, int type, int protocol);
//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int main(int argc, char **argv)
{
int c_fd;//1. socket
int n_read;//4. read
char readBuf[128];//4. read
char *msg = "I'm a msg from Client";//3. write
//1. socket 获取/创建套接字
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
//2.connect//连接服务器
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(8989);
inet_aton("192.168.2.13",&c_addr.sin_addr);//服务器的ip地址
if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){//连接时会阻塞,直到出结果为之
perror("connect");
exit(-1);
}
//3. write//写
write(c_fd,msg,strlen(msg));
//4. read//读
n_read = read(c_fd, readBuf, 128);
if(n_read == -1){
perror("read");
}else{
printf("get message from server:%d,%s\n",n_read,readBuf);//打印收到了多少个字节的消息,和消息的内容
}
return 0;
}
8. 实现双方聊天(451.8)
- SOCKET/server3.c(服务器可以一直接收多个客户端的连接,并和某个客户端互发消息)
#include <stdio.h>//printf
#include <sys/types.h>//socket bind /* See NOTES */
#include <sys/socket.h>//socket bind
//#include <linux/in.h>//struct sockaddr_in//冲突
#include <arpa/inet.h>//htons
#include <netinet/in.h>//inet_aton
#include <stdlib.h>//exit
#include <string.h>//memset
#include <unistd.h>//read write
//int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*struct in_addr {
__be32 s_addr;
};*/
//int listen(int sockfd, int backlog);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//fd,客户端地址,客户端地址长度,返回新的建立的socket的通道
int main(int argc, char **argv)
{
int s_fd;//1. socket
int c_fd;//4. accept
int n_read;//5. read
char readBuf[128];//5. read
// char *msg = "I get your connection";
char msg[128] = {0};//6. write
struct sockaddr_in s_addr;//2. bind
struct sockaddr_in c_addr;//2. bind
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc != 3){//如果没有输入参数 会提示
printf("param is not good\n");
exit(-1);
}
//1. socket 获取套接字
s_fd = socket(AF_INET, SOCK_STREAM, 0);//IPv4 因特网域,TCP协议,自动选择type类型对应的默认协议(也可用宏:IPPROTO_TCP TCP)
if(s_fd == -1){//判断建立socket通道是否成功
perror("socket");//把错误的问题打出来
exit(-1);//退出这个程序
}
//2. bind 绑定
s_addr.sin_family = AF_INET;//协议族,跟domain一致
s_addr.sin_port = htons(atoi(argv[2]));//字符串转换为整形数
inet_aton(argv[1],&s_addr.sin_addr);//本机ip地址由字符串形式to网络格式//ifconfig
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));//把ip地址端口号和s_fd绑定起来
//3. listen
listen(s_fd,10);//监听10个连接
//4. accept 接受连接
int clen = sizeof(struct sockaddr_in);
while(1){//不让程序退出 一直循环进行//不要在while(1)里定义变量
//同时收和发
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);//如果没连接到,会卡在这
if(c_fd == -1){
perror("accept");
}
printf("get connected: %s\n",inet_ntoa(c_addr.sin_addr));//连上后打印 “已连接” 和客户端的 ip地址
if(fork() == 0){//在网络服务进程中父进程等待客户端的服务请求 当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达
//6. write
if(fork()==0){//如果要用两个while(1),要么用多线程,要么用fork创建子进程
while(1){
memset(msg,0,sizeof(msg));
printf("input: ");
//gets(msg);//gets不安全,已经被废弃
fgets(msg,sizeof(msg),stdin);//阻塞
msg[strcspn(msg, "\n")] = '\0';//移除输入中的换行符
write(c_fd,msg,strlen(msg));//6. write
}
}
//5. read
while(1){
memset(readBuf,0,sizeof(readBuf));//每次写之前清空
n_read = read(c_fd, readBuf, 128);//阻塞
if(n_read == -1){
perror("read");
}else{
printf("get message from client:%d,%s\n",n_read,readBuf);//打印收到了多少个字节的消息,和消息的内容
}
}
}
}
return 0;
}
- SOCKET/client2.c(多个客户端分别连接服务器,并和服务器互发消息)
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
//#include <linux/in.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>//read write
//int socket(int domain, int type, int protocol);
//int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int main(int argc, char **argv)
{
int c_fd;//1. socket
int n_read;//4. read
char readBuf[128];//4. read
//char *msg = "I'm a msg from Client";//3. write
char msg[128] = {0};
if(argc != 3){//如果没有输入参数 会提示
printf("param is not good\n");
exit(-1);
}
//1. socket 获取/创建套接字
c_fd = socket(AF_INET, SOCK_STREAM, 0);
if(c_fd == -1){
perror("socket");
exit(-1);
}
//2.connect 连接服务器
struct sockaddr_in c_addr;
memset(&c_addr,0,sizeof(struct sockaddr_in));
c_addr.sin_family = AF_INET;
c_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&c_addr.sin_addr);//服务器的ip地址
if(connect(c_fd, (struct sockaddr *)&c_addr,sizeof(struct sockaddr)) == -1){//连接时阻塞 成功后往下
perror("connect");
exit(-1);
}
while(1){//不让程序退出 一直循环进行
//同时发和收
//3. write 写
if(fork()==0){//如果要用两个while(1),要么用多线程,要么用fork创建子进程
while(1){
memset(msg,0,sizeof(msg));//每次写之前清空
printf("input: ");
//gets(msg);//gets不安全,已经被废弃
fgets(msg,sizeof(msg),stdin);//阻塞
msg[strcspn(msg, "\n")] = '\0';//移除输入中的换行符
write(c_fd,msg,strlen(msg));
}
}
//4. read 读
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd, readBuf, 128);//阻塞
if(n_read == -1){
perror("read");
}else{
printf("get message from server:%d,%s\n",n_read,readBuf);//打印收到了多少个字节的消息,和消息的内容
}
}
}
return 0;
}
9. 多方消息收发(452.9)
- 上节课的逻辑:shell终端获得输入,并不知道是哪个子进程的输入,所以返回给客户端也并不知道是哪个(子进程之间存在资源竞争)
- SOCKET/server4.c(自动主动回复给客户端消息,类似心跳包)
#include <stdio.h>//printf
#include <sys/types.h>//socket bind /* See NOTES */
#include <sys/socket.h>//socket bind
//#include <linux/in.h>//struct sockaddr_in//冲突
#include <arpa/inet.h>//htons
#include <netinet/in.h>//inet_aton
#include <stdlib.h>//exit
#include <string.h>//memset
#include <unistd.h>//read write
//int socket(int domain, int type, int protocol);
//int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*struct in_addr {
__be32 s_addr;
};*/
//int listen(int sockfd, int backlog);
//int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);//fd,客户端地址,客户端地址长度,返回新的建立的socket的通道
int main(int argc, char **argv)
{
int s_fd;//1. socket
int c_fd;//4. accept
int n_read;//5. read
int mark = 0;//第几个客户端
char readBuf[128];//5. read
// char *msg = "I get your connection";
char msg[128] = {0};//6. write
struct sockaddr_in s_addr;//2. bind
struct sockaddr_in c_addr;//2. bind
memset(&s_addr,0,sizeof(struct sockaddr_in));
memset(&c_addr,0,sizeof(struct sockaddr_in));
if(argc != 3){//如果没有输入参数 会提示
printf("param is not good\n");
exit(-1);
}
//1. socket 获取套接字
s_fd = socket(AF_INET, SOCK_STREAM, 0);//IPv4 因特网域,TCP协议,自动选择type类型对应的默认协议(也可用宏:IPPROTO_TCP TCP)
if(s_fd == -1){//判断建立socket通道是否成功
perror("socket");//把错误的问题打出来
exit(-1);//退出这个程序
}
//2. bind 绑定
s_addr.sin_family = AF_INET;//协议族,跟domain一致
s_addr.sin_port = htons(atoi(argv[2]));//字符串转换为整形数
inet_aton(argv[1],&s_addr.sin_addr);//本机ip地址由字符串形式to网络格式//ifconfig
bind(s_fd,(struct sockaddr *)&s_addr,sizeof(struct sockaddr_in));//把ip地址端口号和s_fd绑定起来
//3. listen
listen(s_fd,10);//监听10个连接
//4. accept 接受连接
int clen = sizeof(struct sockaddr_in);
while(1){//不让程序退出 一直循环进行//不要在while(1)里定义变量
//同时收和发
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);//如果没连接到,会卡在这
if(c_fd == -1){
perror("accept");
}
mark++;
printf("get connected: %s\n",inet_ntoa(c_addr.sin_addr));//连上后打印 “已连接” 和客户端的 ip地址
if(fork() == 0){//在网络服务进程中父进程等待客户端的服务请求 当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达//一有客户端接入就调用子进程
//6. write
if(fork()==0){//如果要用两个while(1),要么用多线程,要么用fork创建子进程
while(1){
sprintf(msg,"welcome NO.%d Client",mark);//每个客户端都能知道其连接有无丢失
write(c_fd,msg,strlen(msg));//6. write//对于服务端都能收到客户端的请求
sleep(3);//类似心跳包,每隔3s发一串话
}
}
//5. read
while(1){//接收每一个客户端发来的消息
memset(readBuf,0,sizeof(readBuf));//每次写之前清空
n_read = read(c_fd, readBuf, 128);//阻塞
if(n_read == -1){
perror("read");
}else{
printf("get message from NO.%d Client :%d,%s\n",mark,n_read,readBuf);//打印收到了多少个字节的消息,和消息的内容
}
}
}
}
return 0;
}
10. 项目运行结果(453.1)
11. 项目研发思路(454.2)
客户端要实现的指令:
- get a.c:获取服务器的文件 a.c
- put a.c:上传文件 a.c 到服务器
- cd a:进入服务器某文件夹 a
- ls:展示服务器有哪些文件和文件夹
- pwd:显示当前文件夹所在路径
- lcd aa:进入客户端本地的某文件夹 aa
- lls:展示客户端本地有哪些文件和文件夹
- quit:退出连接
服务端去解析这些指令,执行这些指令,返回这些指令的结果
- 自行搜索 “Linux 判断文件是否存在” 的函数,写 demo 测试,再整合进项目中
- recv 和 read 区别:
- recv 第四个参数 MSG_PEEK 就可以多次读,其返回值等0时表示客户端连接断开,小于0读数据失败
- read 只能读一次套接字中的数据就
12. 项目指导(455.3)
- 服务端
- 客户端