socket是套接字,通过套接字,进行网络数据的收和发
套接字就像网络中的“手机”
主机字节序列和网络字节序列
主机字节序列分为大端字节序和小端字节序,不同的主机采用的字节序列可能不同。大端字节序是指一个整数的高位字节存储在内存的低地址处,低位字节存储在内存的高地址处。小端字节序则是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。在两台使用不同字节序的主机之间传递数据时,可能会出现冲突。
所以,在将数据发送到网络时规定整形数据使用大端字节序,所以也把大端字节序成为网络字节序列。对方接收到数据后,可以根据自己的字节序进行转换。
发送整数之前,要转成网络字节序列
Linux 系统提供如下 4 个函数来完成主机字节序和网络字节序之间的转换
#include <netinet/in.h>
uint32_t htonl(uint32_t hostlong); // 长整型的主机字节序转网络字节序
uint32_t ntohl(uint32_t netlong); // 长整型的网络字节序转主机字节序
uint16_t htons(uint16_t hostshort); // 短整形的主机字节序转网络字节序
uint16_t ntohs(uint16_t netshort); // 短整型的网络字节序转主机字节序
套接字地址结构
通用 socket 地址结构
socket 网络编程接口中表示 socket 地址的是结构体 sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr//通用的
{
sa_family_t sa_family;
char sa_data[14];//占位的作用
};
sa_family 成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。常见的协议族和对应的地址族如下图所示
不同的协议,地址不一样,宿舍是宿舍号,小区是哪一栋楼几零几。
专用 socket 地址结构
TCP/IP 协议族有 sockaddr_in 和 sockaddr_in6 两个专用 socket 地址结构体,它们分别用于 IPV4 和 IPV6:
/*
sin_family: 地址族 AF_INET
sin_port: 端口号,需要用网络字节序表示
sin_addr: IPV4 地址结构:s_addr 以网络字节序表示 IPV4 地址
*/
struct in_addr
{
u_int32_t s_addr;
};
struct sockaddr_in//IPV4
{
sa_family_t sin_family;
u_int16_t sin_port;
struct in_addr sin_addr;
};
struct in6_addr
{
unsigned char sa_addr[16]; // IPV6 地址,要用网络字节序表示 };
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址族:AF_INET6
u_inet16_t sin6_port; // 端口号:用网络字节序表示
u_int32_t sin6_flowinfo; // 流信息,应设置为 0
struct in6_addr sin6_addr; // IPV6 地址结构体
u_int32_t sin6_scope_id; // scope ID,尚处于试验阶段
};
IP 地址转换函数
通常,人们习惯用点分十进制字符串表示 IPV4 地址,但编程中我们需要先把它们转化为整数方能使用,下面函数可用于点分十进制字符串表示的 IPV4 地址和网络字节序整数表示的 IPV4 地址之间的转换
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp); // 字符串表示的 IPV4 地址转化为网络字节序
char* inet_ntoa(struct in_addr in); // IPV4 地址的网络字节序转化为字符串表示
整数有4个字节。把每一个字节转换为整型。
每个字节的数肯定小于255
字符串表示:点和十进制
网络编程接口
#include <sys/types.h>
#include <sys/socket.h>
/*************************************************************
socket()创建套接字,成功返回套接字的文件描述符,失败返回-1
domain: 设置套接字的协议簇, AF_UNIX AF_INET AF_INET6
type: 设置套接字的服务类型 SOCK_STREAM SOCK_DGRAM
protocol: 一般设置为 0, 表示使用默认协议
*************************************************************/
int socket(int domain, int type, int protocol);
/*************************************************************
bind()将 sockfd 与一个 socket 地址绑定,成功返回 0,失败返回-1
sockfd 是网络套接字描述符
addr 是地址结构
addrlen 是 socket 地址的长度
**************************************************************/
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/*************************************************************
listen()创建一个监听队列以存储待处理的客户连接,成功返回 0,失败返回-1
sockfd 是被监听的 socket 套接字
backlog 表示处于完全连接状态的 socket 的上限
**************************************************************/
int listen(int sockfd, int backlog);
/*************************************************************
accept()从 listen 监听队列中接收一个连接,成功返回一个新的连接 socket,
该 socket 唯一地标识了被接收的这个连接,失败返回-1
sockfd 是执行过 listen 系统调用的监听 socket
addr 参数用来获取被接受连接的远端 socket 地址
addrlen 指定该 socket 地址的长度
*************************************************************/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*************************************************************
connect()客户端需要通过此系统调用来主动与服务器建立连接,
成功返回 0,失败返回-1
sockfd 参数是由 socket()返回的一个 socket。
serv_addr 是服务器监听的 socket 地址
addrlen 则指定这个地址的长度
*************************************************************/
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
/*************************************************************
close()关闭一个连接,实际上就是关闭该连接对应的 socket
*************************************************************/
int close(int sockfd);
/**************************************************************
TCP 数据读写:
recv()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大小
send()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长
度
flags 参数为数据收发提供了额外的控制
**************************************************************/
ssize_t recv(int sockfd, void *buff, size_t len, int flags);
ssize_t send(int sockfd, const void *buff, size_t len, int flags);
/**************************************************************
UDP 数据读写:
recvfrom()读取 sockfd 上的数据,buff 和 len 参数分别指定读缓冲区的位置和大
小
src_addr 记录发送端的 socket 地址
addrlen 指定该地址的长度
sendto()往 socket 上写入数据,buff 和 len 参数分别指定写缓冲区的位置和数据长
度
dest_addr 指定接收数据端的 socket 地址
addrlen 指定该地址的长度
**************************************************************/
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, void *buff, size_t len, int flags,
struct sockaddr* dest_addr, socklen_t addrlen);
TCP 编程流程
TCP 提供的是面向连接的、可靠的、字节流服务。
TCP 的服务器端和客户端编程流程如下:
服务器端是被动等着别人去链接它,客户端是主动去链接服务器端。
socket()方法(手机) 是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
服务端
bind()方法(手机号码) 是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值,一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在Linux 上,1024 以内的端口号,只有 root 用户可以使用。
listen()方法(开机) 是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接,一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。
accept()方法(接通电话) 处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
rece()方法(收听内容)
send()方法(和你说话)
close()方法(挂断电话)
客户端
socket()方法(手机)
可以指定,但是这里不用指定,主机随机分配临时端口,但是客户端不需要指定。
connect()方法(拨打电话) 一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接。
send()方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入到发送缓冲区中的数据长度。
recv()方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
close()方法用来关闭 TCP 连接。此时,会进行四次挥手。
TCP 服务端代码 TcpServer.c 示例如下
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);//创建套接字,指定地址族,流式服务类型
assert( sockfd != -1 );
struct sockaddr_in saddr;//ipv4专用的 确定ip和端口,相当于手机号
memset( &saddr, 0, sizeof(saddr) );//结构的内容全部清空,因为后面有占位有多余的部分
saddr.sin_family = AF_INET;//地址族
saddr.sin_port = htons(6000);//htons 将主机字节序转换为网络字节序
saddr.sin_addr.s_addr = inet_addr( "127.0.0.1" ); //回环地址,把字符串转换为无符号整型
int res = bind(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));//命名指定
assert( res != -1 );
res = listen(sockfd, 5);//5是相当于处理窗口的个数,相当于开机
assert( res != -1 );
while( 1 )//服务器循环接收客户端的连接
{
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int c = accept( sockfd, (struct sockaddr*)&caddr, &len ); //阻塞,链接套接字
if(c == -1)
{
perror("accept error ");
continue;
}
printf("accept c = %d\n", c);
char data[128] = {0};
int n = recv(c, data, 127, 0);//阻塞,读到几个字节数,从c上接收
printf("n = %d, buff = %s\n", n, data);
send(c, "OK", 2, 0);
close(c);
}
close(sockfd);
exit(0);
}
这个IP地址(127.0.0.1)是测试用的,自测,能收能发数据,每个人都有,自己给自己发数据。
一个客户端对应一个C,客户端建立socket和服务端打招呼,服务端返回c。 c是链接套接字
TCP 客户端代码 TCPClient.c 示例如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
assert(sockfd != -1);
struct sockaddr_in saddr;//建立套接字地址结构,指定服务端的,可以但是不需要确定自己的,自己的不需要,不影响,我们是打电话给服务器,自己手机号多少不重要!系统会分配临时端口,IP地址你是多少就多少
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);//我们要去链接服务器的6000端口,不是自己的端口
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd, (struct sockaddr*)&saddr, sizeof(saddr));
//打电话不一定成功
assert(res != -1);
printf("please input: ");
char buff[128] = {0};
fgets(buff, 128, stdin);
send(sockfd, buff, strlen(buff) - 1, 0);//用write也可以
char data[128] = {0};
int n = recv(sockfd, data, 127, 0);//用read也可以
printf("%s\n", data);
close(sockfd);
exit(0);
}
运行截图如下
n:端口号
a:地址
t:连接
p:pid
复位报文段
在某些特殊条件下,TCP 连接的一端会向另一端发送携带 RST 标志的报文段,即复位报文段,已通知对方关闭连接或重新建立连接。这里介绍一下三种情况:
1)当户端端程序访问一个不存在的端口时,目标主机给它发送一个复位报文段。
2)异常终止连接。正常情况下,数据交换完成之后,一方给另一方发送 FIN 结束报文段。TCP 提供了异常终止一个连接的方法,即给对方发送一个复位报文段。一但发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。应用程序可以使用 socket 选项SO_LINGER 来设置发送复位报文段,以异常终止连接。
3) 处理半打开连接。例如 TCP 一端关闭了连接,由于网络故障对方没有收到结束报文,对方误以为连接仍然正常。处于这种状态的连接称为半打开连接。此时如果对端向连接写入数据,则会收到本端回复的复位报文段。
交互数据流与成块数据流
TCP 按照携带应用程序数据长度可以分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数据的应用程序对实时性要求极高,比如 telnet、ssh 等。成块数据的长度则通过为 TCP 报文段允许的最大数据长度。使用成块数据的应用程序对传输效率要求高,比如 FTP。
带外数据
有些传输层协议具有带外(out of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。
UDP 没有实现带外数据传输,TCP 也没有真正的带外数据。不过 TCP 利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种传输紧急数据的方式。一般只有一个字节数据。