网络编程

网络传输模型分为七层:物、链、网、传、会、表、应

采用TCP/IP协议时,可简化为四层:网络接口、网络、传输、应用

前置知识

TCP:可靠传输,三次握手连接,一对一,面向字节流,可靠性高的应用(聊天软件)

UDP:不可靠传输,不需要建立连接,n对n,面向数据报,实时性好(视频会议)

套接字:表示通信的端点。套接字相当于电话,IP地址为总机号码,端口号为分机号码。

文件描述符(对应于window的句柄):系统分配给文件或套接字的整数

文件和套接字一般经过创建过程才会被分配文件描述符,输入输出对象即使未经过特殊的创建过程,程序开始运行后也会被自动分配文件描述符。

网络编程入门↓↓↓

网络编程:编写程序让两台联网的计算机相互交换数据。在我们不需要考虑物理连接的情况下,我们只需要考虑如何编写传输软件。操作系统提供了名为“套接字”,套接字是网络传输传输用的软件设备

基于Windows平台的网络编程

1、socket函数创建套接字

1
2
#include <sys/socket.h> 
int socket(int domain, int type, int protocol);
  • 成功时返回文件描述符,失败时返回-1.
  • domain,套接字中使用的协议族信息;
  • type,套接字数据传输类型信息;
  • protocol,计算机间通信中使用的协议信息
  • 协议族(Protocol Family): PF_INET,IPV4互联网协议族 ;PF_INET6,IPV6互联网协议族
  • PF_LOCAL,本地通信的Unix协议族 PF_PACKET,底层套接字的协议族 PF_IPX,IPX Novell协议族
1、面向连接的套接字(基于字节)

向socket第二个参数传递SOCK_STREAM,创建面向连接的套接字
特点:

  • 传输过程中数据不会丢失、按序传输数据、传输的数据不存在数据边界,
  • 套接字连接必须一一对应
  • 收发数据的套接字内部有缓冲(buffer),即字节数组 面向连接的套接字会根据接收端的状态传输数据

如:

1
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
2、面向消息的套接字SOCK_DGRAM

不可靠、不按顺序、无连接、有数据边界、以高速为目的
限制每次传输的数据大小

1
int udp_socket = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
协议的最终选择

socket函数的第三个参数决定最终采用的协议,第三个参数大多取0,除非同一协议族中存在多个数据传输方式相同的协议

2、bind函数分配地址信息(IP地址和端口号)

1
2
#include <sys/socket.h>  
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
  • 成功返回0,失败返回-1。
  • sockfd 要分配地址信息(IP地址和端口号)的套接字文件描述符
  • myaddr 存有地址信息的结构体变量地址值
  • adddrien 第二个结构体变量的长度
  • 地址信息的表示
  • AF_INET,IPV4网络协议中使用的地址族
  • AF_INET6,IPV6网络协议中使用的地址族

3.相关结构体

1
2
3
4
5
6
7
8
struct sockaddr_in 
{
sa_family_t sin_family;//地址族
uint16_t sin_port;//16位tcp/udp端口号,以网络字节序保存
struct in_addr sin_addr;//32位IP地址,以网络字节序保存
char sin_zero[8];//不使用,必需为0,
/* 为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员 */
};
1
2
3
4
struct in_addr  
{
in_addr_t s_addr;//32位IPV4地址
};

POSIX是UNIX系列操作系统的可移植操作系统接口,定义了一些其他数据类型
- sa_family_t 地址族
- socklen_t 长度
- in_addr_t IP地址,声明为uint32_t
- in_port_t 端口号,声明为uint16_t

4.网络字节序与地址变换

字节序和网络字节序:
大端序:高位字节存放到低位地址 【高位字节值存放到低位地址,先存高位值】
小端序:高位字节存放到高位地址
IntelCPU系列用小端
网络字节序统一为大端序

字节序转换:

1
2
3
4
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

h代表主机,n代表网络,s指short,l指long,to就是to
除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题

5.网络地址的初始化和分配

1
2
#include <arpa/inet.h> 
in_addr_t inet_addr(const char * string);

成功返回 32位大端序整数型值,失败时返回INADDR_NONE,还可以检测无效IP

1
2
#include <arpa/inet.h> 
int inet_aton(const char * string, struct in_addr * addr);
  • string 含有需转换的IP地址信息的字符串地址值
  • addr 将保存转换结果的in_addr结构体变量的地址值

aton和addr函数功能完全相同,利用了in_addr结构体,使用频率更高,但Windows没有此函数

6.网络地址初始化-服务端

1
2
3
4
5
6
7
struct sockaddr_in addr; 
char * serv_ip = "211.217.168.13"; //声明IP地址字符串
char * serv_port = "9190"; //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员置零
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); //基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); //基于字符串的端口号初始化

7.accept

1
2
3
#include <sys/socket.h> 
int accept(int sockfd, struct sockaddr *addr,
socklen_t *addrlen);
  • 成功时返回文件描述符,失败时返回-1.
  • sock 服务器套接字的文件描述符
  • addr 保存发起连接请求的客户端地址信息的变量地址值
  • 调用函数后向传递来的地址变量参数填充客户端地址信息
  • addrlen 第二个参数addr结构体的长度,但是存有长度的变量地址。
  • 函数调用完成后,该变量即被填入客户端地址长度

函数调用成功时,accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,套接字自动创建并自动与发起连接的客户端建立连接

8.connect

1
2
#include <sys/socket.h> 
int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);
  • 成功返回0,失败返回-1。

  • sock 客户端套接字的文件描述符

  • servaddr 保存目标服务器端地址信息的变量地址值

  • addrlen 以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度

客户端调用connect函数后,服务器端接收连接请求或中断连接请求才会返回
客户端的IP地址和端口在调用connect函数时自动分配

Linux下

参考1

参考2

TCP

服务端

流程:

  1. 调用socket函数创建监听socket
  2. 调用bind函数将socket绑定到某个IP和端口号组成的二元组上
  3. 调用listen函数开启监听
  4. 当有客户端连接请求时,调用accept函数接受连接,产生一个新的socket(与客户端通信的socket)
  5. 基于新产生的socket调用send或recv函数开始与客户端进行数据交流
  6. 通信结束后,调用close函数关闭socket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include<iostream>
#include<sys/types.h>//基本系统数据类型
#include<arpa/inet.h>//网络信息转换(IP地址转换)
#include <unistd.h> //与系统交互的函数
#include <string.h>
using namespace std;

int main(){
//1.创建socket (用于监听),成功则返回文件描述符,失败返回-1
int sockfd=socket(AF_INET,SOCK_STREAM,0);
//常见的AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域
//套接口可能的类型,SOCK_STREAM字节流,SOCK_DGRAM数据报、SOCK_SEQPACKET有序分组、SOCK_RAW原始套接口
//传输协议TCP/UDP,这里默认0
if(sockfd==-1){
perror("socket");
return -1;
}

//2.绑定ip和端口,成功则0,失败-1

//初始化服务器地址
struct sockaddr_in saddr;
saddr.sin_family=AF_INET;

saddr.sin_addr.s_addr=INADDR_ANY; //即0,代表任意地址,表示任意ip都能访问到我们

//将主机端口号为9999的转换为网络字节序 主机字节序->网络字节序
unsigned short sport=htons(9999);
saddr.sin_port=sport;

//如果只想在本机上进行访问,bind函数地址可以使用本地回环地址
//如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址
//如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0

//创建套接字后,为其分配地址
int ret=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));

if(ret== -1){
perror("bind");
return -1;
}

//3.监听
//将套接字转为可接受连接方式
ret=listen(sockfd,8);
if(ret== -1){
perror("listen");
return -1;
}

cout<<"开始监听了!"<<endl;
//4.接收客户端连接

//阻塞函数,如果没有客户端连接,会一直阻塞在这
//clientaddr 是已经连接进来的客户端的信息
struct sockaddr_in clientaddr;
socklen_t len=sizeof(clientaddr);
// accept里面的第三个参数长度,该数据类型是指针,和bind不一样
//受理连接请求,并且在没有连接请求的情况调用该函数,不会返回。直到有连接请求为止。
int accfd=accept(sockfd,(struct sockaddr*)&clientaddr,&len);
if(accfd==-1){
perror("accept");
return -1;
}

//打印客户端信息,如果有的话
char clientIP[16];
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,clientIP,sizeof(clientIP));
//将网络字节序转换成主机字节序打印
//xxx.xxx.xxx.xxx 15,最后有一个\0,一共16个字节
unsigned short clientPort=ntohs(clientaddr.sin_port);
printf("client ip is %s,port is %d \n",clientIP,clientPort);

//5.通信
//获取客户端的数据
char recvBuf[1024]={0};
while(1){
int lens=read(accfd,recvBuf,sizeof(recvBuf));
if(lens==-1){
perror("read");
return -1;
}else if(lens >0){
//读到了数据,并输出
printf("recv client data: %s \n",recvBuf);
}else if(lens==0){
//客户端断开连接
printf("客户端断开连接。。。");
break;
}

//给客户端发送数据
char *data="hello, i am server";
write(accfd,data,strlen(data));

// 服务端可以不用sleep,因为客户端睡眠1秒才发送,这1秒内服务器端处于阻塞状态,没有数据可读
//sleep(1);
}

//关闭文件描述符
close(accfd);
close(sockfd);
return 0;
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include<iostream>
#include<sys/types.h>//基本系统数据类型
#include<arpa/inet.h>//网络信息转换(IP地址转换)
#include <unistd.h> //POSIX系统API访问
#include <string.h>
using namespace std;

int main(){

//1.创建套接字
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
perror("socket");
return -1;
}

//2.连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family=AF_INET;
inet_pton(AF_INET,"192.168.78.161",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port=htons(9999);
int ret=connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
if(ret==-1){
perror("connect");
return -1;
}

//3.通信
char recvBuf[1024]={0};
while(1){

char *data="hello,i am client";
//给服务器发送数据
write(sockfd,data,strlen(data));

sleep(1);

//读数据
int len=read(sockfd,recvBuf,sizeof(recvBuf));
if(len==-1){
perror("read");
return -1;
}else if(len>0){
//读到了数据
printf("recv server data: %s \n",recvBuf);
}else if(len== 0){
//服务器断开连接
printf("server closed!");
break;
}
}

//关闭连接
close(sockfd);
return 0;
}
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2023-2025 是羽泪云诶
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信