Socket 三要素
1.通信的目的地址;
2.使用的端口号;
3.使用的传输层协议(如 TCP、UDP)
Socket 通信模型
服务端实现
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#define SERVER_PORT 8089
using namespace std;
int main(int argc, char const *argv[])
{
int sock; // mailbox
struct sockaddr_in server_addr;
//Create a mailbox
sock = socket(AF_INET, SOCK_STREAM, 0);
// clear tags, write addresses and port
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET; //选择协议族IPV4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //监听本地所有IP地址
server_addr.sin_port = htons(SERVER_PORT); //绑定端口号
// label affixed to the receiving mailbox
bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(sock, 128); //the number of clients
// ready to mail
cout<<"wait for client"<<endl;
int done = 1;
while(done){
struct sockaddr_in client;
int client_socket, len;
char client_ip[64];
char buf[256];
socklen_t client_addr_len;
client_addr_len = sizeof(client);
client_socket = accept(sock, (struct sockaddr *)&client, &client_addr_len);
// print the IP address amd port of client
cout<<"client IP: "<<inet_ntop(AF_INET, &client.sin_addr.s_addr, client_ip, sizeof(client_ip))
<<" port: "<<ntohs(client.sin_port)<<endl;
//read client->message
len = read(client_socket, buf, sizeof(buf)-1);
buf[len] = '\0';
cout<<"receive: "<< buf <<" len:"<<len <<endl;
len = write(client_socket, buf, len);
cout<<"write finished. len: "<<len<<endl;
close(client_socket);
}
return 0;
}
客户端实现
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
using namespace std;
#define SERVER_PORT 8089
#define SERVER_IP "127.0.0.1"
int main(int argc, const char* argv[]) {
int sockfd;
const char *message;
struct sockaddr_in servaddr;
int n;
char buf[64];
if(argc!=2){
fputs("Usage: ./echo_client message \n", stderr);
exit(1);
}
message = argv[1];
cout<<"message: "<< message <<endl;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
memset(&servaddr, '\0', sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
servaddr.sin_port = htons(SERVER_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, message, strlen(message));
n = read(sockfd, buf, sizeof(buf)-1);
if (n > 0)
{
buf[n] = '\0';
printf("receive: %s\n", buf);
}
else
{
perror("error!!!");
}
printf("finished.\n");
close(sockfd);
return 0;
}
socket 连接过程(TCP)
服务器端:
1.首先调用 socket()
函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 Socket ,接着调用 bind()
函数,给这个 Socket 绑定一个 IP 地址和端口;
绑定端口的目的:当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们。
绑定 IP 地址的目的:一台机器是可以有多个网卡的,每个网卡都有对应的 IP 地址,当绑定一个网卡时,内核在收到该网卡上的包,才会发给我们;
2.绑定完 IP 地址和端口后,就可以调用 listen()
函数进行监听,此时对应 TCP 状态图中的 listen,如果我们要判定服务器中一个网络程序有没有启动,可以通过 netstat
命令查看对应的端口号是否有被监听。
3.服务端进入了监听状态后,通过调用 accept()
函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。
客户端:
客户端在创建好 Socket 后,调用 connect()
函数发起连接,该函数的参数要指明服务端的 IP 地址和端口号,然后 TCP 三次握手。
在 TCP 连接的过程中,服务器的内核实际上为每个 Socket 维护了两个队列:
一个是「还没完全建立」连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于 syn_rcvd 的状态;
一个是「已经建立」连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于 established 状态;
当 TCP 全连接队列不为空后,服务端的 accept()
函数,就会从内核中的 TCP 全连接队列里拿出一个已经完成连接的 Socket 返回应用程序,后续数据传输都用这个 Socket。
监听的 Socket 和真正用来传数据的 Socket 是两个:
一个叫作监听 Socket;
一个叫作已连接 Socket;
连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过 read()
和 write()
函数来读写数据。
至此, TCP 协议的 Socket 程序的调用过程就结束了。
小林coding
套接字概念
Socket 在Linux环境下,用于表示进程间网络通信的特殊文件类型。本质为内核借助缓冲区形成的伪文件。
既然是文件,那么可以使用文件描述符引用套接字。
Linux系统将其封装成文件的目的是为了统接口,使得读写套接字和读写文件的操作一致。
区别是文件主要应用于本地持久化数据的读写,而套接字多应用于网络进程间数据的传递。
创建 Socket 的时候,可以指定网络层使用的是 IPv4 还是 IPv6,传输层使用的是 TCP 还是 UDP.
在TCP/IP协议中,“IP 地址+TCP或UDP端口号”唯-标识网络通讯中的一个进程。
“IP 地址+端口号”就对应一个 sockct,欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一 个连接。 因此可以用Sockst来描述网络连接的一对一 关系。
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
Socket 编程基础
网络字节序(大小端)
网络传输中要多字节数据要进行主机/网络字节序转换的原因
网络字节序中,发送端发送的第一个字节是高位字节(取自内存的低地址),接收端收到后存入低位地址,由于主机的CPU架构不同,导致不同主机的字节序有所不同,因此对于多字节数据,在网络传输中需要进行主机/网络字节序的转换,接收端/发送端 才能正确的解析出多字节数据。
网络字节序(大端字节序)的定义:规定使用大端字节序(网络字节序)作为标准:接收端接收到的第一个字节是发送端的高位字节,存放到低位地址。
大端字节序的主机:按照从左往右的字节顺序将数据存储在内存中,低位字节存储在高位地址,高位字节存储在低位地址。
小端字节序的主机:按照从左往右的字节顺序将数据存储在内存中,低位字节存储在低位地址,高位字节存储在高位地址。
网络字节序
由于不同的主机架构,字节序有所不同,网络通信中,规定使用大端字节序(网络字节序)作为标准:接收端接收到的第一个字节是发送端的高位字节,存放到低位地址。因此需要发送端/接收端进行主机字节序和网络字节序的转换,以确保数据的正确传输和解析。
发送端:
发送的第一个字节是高位字节(取自内存中的低位地址),因此多字节数据在进行网络数据传输时,需要将主机字节序转为网络字节序。
接收端:
接收的第一个字节(高位字节)存放在低位地址。因此,需要将网络字节序转为主机字节序。
htonl()、htons() /* 主机字节序转化为网络字节序 */
ntohl()、ntohs() /* 网络字节序转化为主机字节序 */
sockaddr 数据结构
很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结构体,为了向前兼容,现在sockaddr 退化成了(void *)
的作用,传递一个地址给函数,至于这个函数是sockaddr_in
还是其他的,由地址族确定,然后函数内部再强制类型转化为所需的地址类型。
通用套接字地址格式:
/* POSIX.1g 规范规定了地址族为2字节的值. */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址 */
struct sockaddr{
sa_family_t sa_family; /* 地址族. 16-bit*/
char sa_data[14]; /* 具体的地址值 112-bit */
};
IPv4 套接字格式地址:
/* IPV4套接字地址,32bit值. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
/* 描述IPV4的套接字地址格式 */
struct sockaddr_in
{
sa_family_t sin_family; /* 16-bit */
in_port_t sin_port; /* 端口口 16-bit*/
struct in_addr sin_addr; /* Internet address. 32-bit */
/* 这里仅仅用作占位符,不做实际用处 */
unsigned char sin_zero[8];
};
IPv4的地址格式定义在netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位端口号和32位IP地址,但是sock API的实现早于ANSI C标准化,那时还没有void *类型,因此这些像bind 、accept
函数的参数都用 struct sockaddr *
类型表示,在传递参数之前要强制类型转换一下:
struct sockaddr_in servaddr;
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)); /* initialize servaddr */
IPv6 套接字地址格式:
struct sockaddr_in6
{
sa_family_t sin6_family; /* 16-bit */
in_port_t sin6_port; /* 传输端口号 # 16-bit */
uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
struct in6_addr sin6_addr; /* IPv6地址128-bit */
uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
};
整个结构体长度是 28 个字节,其中流控信息和域 ID 先不用管,这两个字段,一个在 glibc 的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是 AF_INET6
,端口同 IPv4
地址一样,关键的地址从 32
位升级到 128
位,这个数字就大到恐怖了,完全解决了寻址数字不够的问题。
本地套接字地址格式:
struct sockaddr_un {
unsigned short sun_family; /* 固定为 AF_LOCAL */
char sun_path[108]; /* 路径名 */
};
各种套接字对比分析:
地址族字段详解
地址族字段,它表示使用什么样的方式对地址进行解释和保存。地址族在 glibc 里的定义非常多,常用的有以下几种:
AF_LOCAL
:表示的是本地地址,对应的是 Unix 套接字,这种情况一般用于本地 socket 通信,很多情况下也可以写成AF_UNIX
、AF_FILE
;
AF_INET
:因特网使用的 IPv4 地址;
AF_INET6
:因特网使用的 IPv6 地址。
这里的 AF_
表示的含义是 Address Family,但是很多情况下,我们也会看到以 PF_
表示的宏,比如 PF_INET
、PF_INET6
等,实际上 PF_
的意思是 Protocol Family,也就是协议族的意思。我们用 AF_xxx
这样的值来初始化 socket 地址,用 PF_xxx
这样的值来初始化 socket。我们在 <sys/socket.h>
头文件中可以清晰地看到,这两个值本身就是一一对应的。
/* 各种地址族的宏定义 */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX PF_UNIX
#define AF_FILE PF_FILE
#define AF_INET PF_INET
#define AF_AX25 PF_AX25
#define AF_IPX PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25 PF_X25
#define AF_INET6 PF_INET6
IP 地址转换函数
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
该函数将一个点分十进制串转换为一个二进制的网络字节顺序的IP地址。如果src没有指向一个合法的点分十进制字符串,那么该函数返回0。成功返回1,失败返回-1。void *dst
用于存放转换后的网络字节序的IP地址。
const char* inet_ntop(int af, const void *src, char *dst, socklen_t size);
该函数将一个二进制的网络字节顺序的IP地址转换为它对应的点分十进制的字符串,并把得到的以null
结尾的字符串复制到dst
。成功返回指向点分十进制的指针,失败返回NULL
。
其中af
代表地址类型,const void *src
是需要被转换的网络字节序的IP地址;char *dst
用于存放转换后的字符串类型的IP地址;socklen_t size
代表数组char *dst
的长度。
af 取值可选为 AF_INET
和 AF_INET6
, 即对应 ipv4 和 ipv6 对应。
其中 inet_pton
和 inet_ntop
不仅可以转换 IPv4 的 in_addr
,还可以转换 IPv6 的 in6_addr
.
因此函数接口是 void *addrptr
。
int inet_aton(const char *string, struct in_addr* addr);
输入参数string
包含ASCII表示的IP地址, 输出参数addr
是将要用新的IP地址更新的结构。如果输入地址不正确,则返回0
;如果成功,返回非零;如果失败,返回-1
char *inet_ntoa(struct in_addr in);
该函数在内部申请了一块空间保存返回的点分十进制的IP地址。因为返回的结果放到静态存储区,所以不需要手动释放。
in_addr_t inet_addr(const char *str);
该函数可将点分十进制的IP地址转换为无符号长整型。