五、Linux——网络
5.1相关概念
IP地址
IP地址主要用于标识网络主机、其他网络设备(如路由器)的网络地址。简单说,IP地址用于定位主机的网络地址。
端口号
在网络通信中,IP地址用于标识主机网络地址,端口号可以标识主机中发送数据、接收数据的进程。简单说:端口号用于定位主机中的进程,
Socket 套接字:
1.TCP : 面向连接 如:A 打电话 B (可靠)
2.UDP: 面向报文 如:A 发短信给 B 数据量大 (不可靠)
TCP/UDP协议对比
- TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前 不需 要建立连接。
- TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。
- TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如 IP电话,实时视频会议等)。
- 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信。
- TCP首部开销20字节;UDP的首部开销小,只有8个字节。
- TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道。
字节序
字节序是指多字节数据在计算机内存中存储或者网络传输时各自字节的存储顺序。
常见字节序分类
- Little endian(小端字节序) :将低序字节存储在起始地址
- Big endian (大端字节序):将高序字节存储在起始地址
例子:在内存中双字0x01020304(DWORD)的存储方式
内存地址4000&4001&4002&4003
LE 04 03 02 01
BE 01 02 03 04
5.2 Socket编程步骤
- 服务器首先启动,稍后某个时刻客户启动,它试图连接到服务器。
- 客户通过 send() 函数给服务器发送一段数据,服务器通过 recv() 函数接收客户发送的数据,并处理该请求,之后通过 send() 函数给客户发回一个响应。
- 这个过程一直持续下去,直到客户关闭 Socket 连接,从而给服务器发送一个 EOF(文件结束)通知。服务器收到后接着也关闭与之相应的 Socket,然后结束运行或者等待新的客户连接。
5.3 相关API
服务器端
- 连接协议
int socket(int domain ,int type ,int protocol);
参数说明:
domain :指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族)
AF_INET IPV4 因特网域 AF_INET IPV6 因特网域 AF_UNIX Unix域 AF_ROUTE 路由器套接字 AF_KEY 密钥套接字 AF_UNSPEC 未指定 type:指定socket的类型
SOCK_STREAM ::流式套接字提供可靠的,面向连接的通信流;它使用TCP协议,从而保证了数据传输的正确性和顺序性。
SOCK_DGRAM:数据报套接字定义了一种无连接的服,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠,无差错的。它使用数据报协议UDP。
SOCK_RAW:允许程序使用底层协议,原始套接字允许对底承协议如IP 或 ICMP进行直接访问,功能强大但使用较为不便,主要用于一些协议的开发。protocol :通常赋值“0 ”
0 选择 type 类型对应的默认协议
IPPROTO_TCP TCP传输协议 IPPROTO_UDP UDP传输协议 IPPROTO_SCTP SCTP传输协议
- IP地址和端口号API
地址准备好
bind()函数:IP号端口号相应描述字赋值函数
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd , const struct sockaddr *addr , socklen_t addrlen);
功能: 用于绑定IP地址和端口号到socketfd
参数说明:
sockfd : 是一个socket描述符
addr :是一个指向包含有本机IP地址及端口号等信息的sockaddr类型的指针,指向要绑定给sockfd的协议地址结构,这个地址结构根据地址创建socket时的地址协议族的不同而不同
实际中常用scockaddr_in结构体把sockaddr替换掉
struct scockaddr_in {
sa_family_t sin_family; //协议族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //IP地址结构体
unisgned char sin_zero[8];
//填充,没有实际意义,只是为跟sockaddr结构在内存中对齐,这样两者才能相互转换
};
//ipv4 对应的是:
struct sckaddr{
unisgned short as_family; //协议族
char sa_data[14]; //IP+端口
};
- 地址转换API
int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);
把字符串形式的“192.168.1.123”转为网络能识别的格式
char* inet_ntoa(struct in_addr inaddr);
把网络格式的IP地址转化为字符串形式
- 监听API
listen()函数 : 监听设置函数
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
- 设置能处理的最大连接数,listen()并未开始接受连线,只是设置sockect的listen模式,listen函数只用于服务器端,服务器进程不知道要与谁连接,因此,它不会主动地要求与某个进程连接,只是一直监听是否有其他客户进程与之连接,然后**响应该连接请求,**并对它做出处理,一个服务进程可以同时处理多个客户进程的连接。
- 主要就两个功能:将一个未连接的套接字转换为一个被动套接字(监听),规定内核为相应套接字排队的最大连接数。
- 内核为任何一个给定监听套接字维护两个队列:
- 未完成连接队列,每个这样的SYN报文段对应其中一项:已由某个客户端发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于 SYN_REVD状态
- 已完成连接队列,每个已完成 TCP三次握手过程的客户端对应其中一项。这些套接字处于ESTABLISHED状态;
参数说明:
sockfd: sockfd是socket系传调用返回的服务器端socket描述符
backlog: backlog指定在请求队列中允许的最大请求数
- 连接API
accept()函数
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:accept 函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠
参数说明:
sockfd: sockfd是socket系统调用返回的服务器端socket 描述符,是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在
addr: 用来返回已连接的对端(客户端)的协议地址
addrled: 客户端地址长度
返回值
**该函数的返回值是一个新的套接字描述符,返回值是表示已连接的套接字描述符,**accept函数接受一个客户端请求后会返回一个新的SOCKFD值,当有不同的客户端同时有不同请求时,会返回不同的SOCKFD的值。这个不同的值和建立SOCKET 时生成的SOCKFD还是不同的。服务器与客户端之间的通信就是在这些不同的SOCKFD上进行的。
而第一个参数是服务器监听套接字描述符。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(表示TCP 三次握手已完成),当服务器完成对某个给定客户的服务时,相应的已连接套接字就会被关闭。
- 数据收发API
在套接字通信中进行字节读取函数: read() , write()。与 I/O中的读取函数略有区别,因为它们输入或输出的字节数比可能比请求的少。
ssize_t write(int fd, const void*buf,sizet nbytes);
ssize_t read(int fd,void *buf,size_t nbyte)
- 数据收发第二套API
在TCP套接字上发送数据函数:有连接
ssize_t send(int s,const void *msg,size_t len,int flags);
//包含3要素:套接字s,待发数据msg,数据长度len
//函数只能对处于连接状态的套接字使用,参数s为已建立好连接的套接字描述
//符,即accept函数的返回值
//参数msg指向存放待发送数据的缓冲区
//参数len为待发送数据的长度,参数flags为控制选项,一般设置为0
在TCP套接字上接收数据函数:有连接
ssize_t recv(int s,void *buf,size_t len,int flags);
//包含3要素:套接字s,接收缓冲区buf,长度len
//函数recv从参数s所指定的套接字描述符(必须是面向连接的套接字)上接收
//数据并保存到参数buf所指定的缓冲区
//参数len则为缓冲区长度,参数flags为控制选项,一般设置为0
- 客户端的connect函数
connect()函数:客户机连接主机
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:该函数用于绑定之后的client 端(客户端),与服务器建立连接参数
参数说明:
sockfd:是目的服务器的 sockect 描述符
addr :是服务器端的IP 地址和端口号的地址结构指针
addrlen:地址长度常被设置为sizeof(struct sockaddr)
返回值:
成功返回0,遇到错误时返回-1,并且errno 中包含相应的错误码
5.4 socket服务端代码实现
服务器建立——可连接代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
//#include<linux/in.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int s_fd;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
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);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(8989);
inet_aton("127.0.0.1", &s_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10); //不断监听外面是否有数据 ,listen配置完后,马上会结束
int clen = sizeof(struct sockaddr_in);
//4.accept
int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen);
//通过套接字连上客户端,后续对连接选项都是用返回值 c_fd 来操作,s_fd 可能还需要接收其他人连接。
if(c_fd == -1){
perror("accept");
}
printf("get connect%s\n",inet_ntoa(c_addr.sin_addr));
//5.read
//6.write
//7.close
printf("connect\n");
while(1);
return 0;
}
注意1:
在 cd /usr/include/ 下收索结构体 struct sockaddr_in grep “struct
sockaddr_in {” *-nir
*-nir表示在当前目录下递归的 找,r是递归 n是显示行号,i是不区分大小写
经过查找,在头文件加上#include<linux/in.h>
注意2:
s_addr.sin_port =(8888); 端口号用户要在5000以上 ,端口号要传到网络上去,所以我们要调用函数来变成网络字节序。
所以我们要知道字节序转换API
#include <netinet/in.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 指定地址让操作系统自己获取
listen(s_fd,10); //不断监听外面是否有数据 ,listen配置完后,马上会结束
int c_fd = accept(s_fd,NULL,NULL); //判断是否有已经三次握手的,如果有,就继续往下,跟客户端进行连接,把客户端信息返回到c_fd;
注意3:
inet_aton(“127.0.0.1”, &s_addr.sin_addr);
//把字符串形式的“127.0.0.1”转为网络能识别的格式
把本机地址转变为网络可识别的地址,需要调用函数来转换,
int inet_aton(const char* straddr , straddr ,struct in_addr *adddrp);
结果显示:
虚拟机的服务端正在等待连接。
在windows下用cmd 命令来连接服务端
连接成功!
我们调用的 socket(AF_INET ,SOCK_STREAM,0);
AF_INET 是TCP协议,我们在windows端cmd下telnet也是TCP协议,所以可以连接上。
printf(“get connect%s\n”,inet_ntoa(c_addr.sin_addr));
char* inet_ntoa(struct in_addr inaddr);
把网络格式的IP地址转化为字符串形式
int c_fd = accept(s_fd , (struct sockaddr *)&c_addr , &clen); 返回客户端信息,打印客户端IP地址
服务器建立——可连接 —— 交互 ——代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int s_fd;
int n_read;
char readBuf[128];
char *msg ="Refuel.CONG";
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
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);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &s_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
int clen = sizeof(struct sockaddr_in);
//4.accept
int c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("get connet%s\n",inet_ntoa(c_addr.sin_addr));
//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));
//7.close
return 0;
}
5.5 socket客户端代码实现
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int k_fd;
int n_read;
char readBuf[128];
char *msg ="message from client \n";
struct sockaddr_in k_addr;
memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据
//1.socket
k_fd = socket(AF_INET ,SOCK_STREAM,0);
if(k_fd == -1){
perror("socket");
exit(-1);
}
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &k_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//3.connet
if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
//4. send
write(k_fd,msg,strlen(msg));
//5. read
n_read = read(k_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message from sever:%d,%s\n",n_read,readBuf);
}
//7.close
return 0;
}
打开服务端 ,服务端等待连接。。。打开客户端,连接成功,同时双端进行交互。
注意——连接的时候IP和端口号要一致:
//服务端
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &s_addr.sin_addr);
//客户端
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(7777);
inet_aton("127.0.0.1", &k_addr.sin_addr);
5.6 实现双方聊天
一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种情求达到时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
服务端代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main(int argc , char **argv)
{
int s_fd;
int c_fd;
int n_read;
char readBuf[128];
char msg[] ={0};
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
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);
if(s_fd == -1){
perror("socket");
exit(-1);
}
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &s_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//2.bind
bind(s_fd,(struct sockaddr *)&s_addr ,sizeof(struct sockaddr_in));
//3.listen
listen(s_fd,10);
int clen = sizeof(struct sockaddr_in);
//4.accept
while(1) //一直接收连接请求
{
c_fd = accept(s_fd,(struct sockaddr *)&c_addr,&clen);
if(c_fd == -1){
perror("accept");
}
printf("get connet :%s\n",inet_ntoa(c_addr.sin_addr));
if(fork() == 0){ // 创建子进程 用来读取客户端
if(fork() == 0){ // 在建子进程,用来与客户端发送
while(1){
memset(msg,0,sizeof(msg));
printf("input(servel) :");
gets(msg);
write(c_fd,msg ,strlen(msg));
}
}
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(c_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message: %d,%s\n",n_read,readBuf);
}
}
break;
}
}
return 0;
}
客户端代码:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<stdlib.h>
#include<string.h>
int main(int argc ,char **argv)
{
int k_fd;
int n_read;
char readBuf[128];
char msg[] ={0};
struct sockaddr_in k_addr;
memset(&k_addr,0,sizeof(struct sockaddr_in));//清空数据
//1.socket
k_fd = socket(AF_INET ,SOCK_STREAM,0);
if(k_fd == -1){
perror("socket");
exit(-1);
}
k_addr.sin_family = AF_INET;
k_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1], &k_addr.sin_addr); //inet_aton 把字符串形式的“127.0.0.1”转为网络能识别的格式的API
//3.connet
if( connect(k_fd,(struct sockaddr *)&k_addr ,sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(-1);
}
//4. send
while(1){
if(fork() == 0){
while(1){
memset(msg,0,sizeof(msg));
printf("input(kehu) :");
gets(msg);
write(k_fd,msg,strlen(msg));
}
}
//5. read
while(1){
memset(readBuf,0,sizeof(readBuf));
n_read = read(k_fd , readBuf ,128);
if(n_read == -1){
perror("read");
}
else{
printf("get message from sever:%d,%s\n",n_read,readBuf);
}
}
}
//7.close
return 0;
}
运行结果:
服务端
客户端