socket网络编程
在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。
- 一个连接对应两个进程,分别对应客户端和服务器端的socket.
背景知识
大端、小端与网络字节序
内存背景知识:
- 一般32位cpu内存地址长度为32位,由于一个内存地址指向1个字节,所以32位系统可以访问2^32 Byte = 4GB 详解为什么32位系统只能用4G内存.
大端(Big-Endian)
数据的高位更加靠近低地址。以数据0x12345678在内存中的存储为例:
内存地址 | 0x00001000 | 0x00001001 | 0x00001002 | 0x00001003 |
---|---|---|---|---|
存放内容 | 0x12 | 0x34 | 0x56 | 0x78 |
小端(Little-Endian)
数据的低位更加靠近低地址。以数据0x12345678在内存中的存储为例:
内存地址 | 0x00001000 | 0x00001001 | 0x00001002 | 0x00001003 |
---|---|---|---|---|
存放内容 | 0x78 | 0x56 | 0x34 | 0x12 |
ip地址
以6.7.8.9为例, 其对应的32位无符号整形为0x06070809,正常在内存中存储的顺序(这里以小端存储为例子)应为 0x09 0x08 0x07 0x06, 但是经过inet_pton将点分十进制Ip地址转为无符号整形时采用了网络字节序,所以0x06070809 在内存中的存储顺序变为 0x06 0x07 0x08 0x09, 此时通过printf打印此值,会打成0x09080706
详见
tcp 网络编程模型
基本概念:
- 网络io: 等价于客户端与服务器建立连接的socket, 可以通过此socket进行读写io操作,从而传递信息
- 多路复用网络io: 通过多线程等方式实现多个客户端同时访问服务器
服务器端
基本模型如下:
socket(...); //创建socket
bind(...); //绑定ip+端口
listen(...);//监听
while(1)//循环处理客户端的连接请求
{
c_fd = accept(...);//三次握手建立连接
while(1)
{
int nr = read(...);//读取客户端消息
if(nr==0)
{
break;
}
process(...);//业务处理
write(...);//发送处理后的数据
}
close(c_fd);//关闭连接
}
创建socket
类似于open()打开文件,返回文件描述符, 创建socket网络通讯端口, 返回socket的文件描述符,可以像文件一下read/write在网络上进行收发数据.
int socket (int domain, int type, int protocol);
domain
:
- AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
- AF_INET6 与上面类似,不过是来用IPv6的地址。
- AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。
type
:
- SOCK_STREAM , 用于TCP可靠传输
- SOCK_DGRAM, 用于UDP不可靠传输
protocol
:
- 0表示默认协议?
绑定ip+端口
- 绑定端口的目的:当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们.
- 绑定 IP 地址的目的:一台机器是可以有多个网卡的,每个网卡都有对应的 IP 地址,当绑定一个网卡时,内核在收到该网卡上的包,才会发给我们.
将socket与addr ip地址进行绑定, 调用函数:
int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen);
addr
:
存放ip地址+port端口号的结构体
addrlen
:
=sizeof(addr), 不是sizeof(struct sockaddr)
监听listen
socket被创建出来的时候都默认是一个主动socket,也就说,内核会认为这个socket之后某个时候会调用connect()主动向别的设备发起连接。这个默认对客户端socket来说很合理,但是监听socket可不行,它只能等着客户端连接自己,因此我们需要调用listen()将监听socket从主动设置为被动,明确告诉内核:你要接受指向这个监听socket的连接请求!
监听socket绑定的ip端口号,是否有连接请求
int listen (int sockfd, int backlog);
backlog
:
相当于客户端可以同时连接服务器的个数, 如果超过了怎么办,进入未决队列?
循环处理客户端的连接请求
- 三次握手建立连接
服务端进入了监听状态后,通过调用 accept() 函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞直到接收到客户的连接请求等待客户端连接的到来。
客户端对server的连接请求会被放入未决连接队列, 服务端通过accept()函数提取队列中的第一个连接请求, 创建并返回一个已连接 Socket, 原始的监听 Socket并不受影响, 未被处理的连接将在队列中排队
int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen);
addr
:
传出参数,返回客户端的地址信息,含IP地址和端口号
返回值:
成功返回一个新的socket文件描述符,用于和客户端通信
- 读取客户端消息
read()
- 业务处理
process()
- 发送处理后的数据
write()
将读取客户端消息,业务处理,发送处理后的数据进行封装:
int recv_send(int c_fd)
{
int nr = read(...);//读取客户端消息
if(nr==-1)
{
exit();
}
else if(nr