1. 科普
1. TCP/UDP的优缺点
- TCP是基于连接的,而UDP是基于数据包的。
2. 使用场景
2. 三次握手和四次挥手
- 网上有太多的详解,比如 详解 TCP 连接的“ 三次握手 ”与“ 四次挥手 ”
- 这个应该比上面更牛逼:https://mp.weixin.qq.com/s/OyyeOTNfWRYE6-CoEUgCJQ
3. 基于TCP的编程模型
3.1 服务器端
- socket(2) 创建一个通讯端点,返回该端点的文件描述符 s_fd
- bind(2)将s_fd和本地地址及其端口号绑定
- listen(2)将s_fd设置为被动状态,监听连接的到来,将到来的连接放入未决连接队列中。
- 未决连接 : 客户端已经连接成功,但服务端还没有处理的连接,以及正在处理但还没处理完的连接
- while(1) {
- accept(2) 从未决队列中取出一个进行处理,返回和客户端的连接描述符 c_fd。如果队列为空就阻塞等待。到这里就说明服务端的三次握手已经成功了
- read(2) 从c_fd中读取客户端请求的数据。没有就阻塞等待
- 用一些库函数等处理客户端请求的数据
- write(2) 将处理的结果返回给客户端
- close(2) 关闭连接描述符c_fd
- }
3.2 客户端
- socket(2) 创建一个通讯端点,返回该端点的文件描述符 s_fd
- connect(2) 使用 s_fd 向服务器发起连接。到这里就说明客户端的三次握手已经成功了
- write(2) 使用 s_fd向服务器发送请求信息
- read(2) 使用 s_fd 阻塞等待服务器的响应消息
- 用一些库函数处理响应的消息
- close(2) 关闭连接描述符s_fd 结束本次连接
3.3 用到的系统调用
1. socket(2)
- 这里主要讲协议家族中的 IPV4/IPV6,和通讯类型中的TCP/UDP
- protocol 一般设置为0
$ man 2 socket
....
# domain 指定网络通信协议家族
The domain argument specifies a communication domain; this selects the protocol family which will be used for communica‐
tion. These families are defined in <sys/socket.h>. The currently understood formats include:
Name Purpose Man page
# 本地通信域,比如linux的图形界面就采用这个AF_UNIX实现
AF_UNIX, AF_LOCAL Local communication unix(7)
# ipv4
AF_INET IPv4 Internet protocols ip(7)
# ipv6
AF_INET6 IPv6 Internet protocols ipv6(7)
....
# type指定通信的类型
The socket has the indicated type, which specifies the communication semantics. Currently defined types are:
# TCP
SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission
mechanism may be supported.
# DDP
SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a fixed maximum length).
SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed
maximum length; a consumer is required to read an entire packet with each input system call.
# 原始套接字
SOCK_RAW Provides raw network protocol access.
....
2. bind(2)
- 将socket(2) 返回的文件描述符和本机地址以及端口号绑定
- 图中 struct sockaddr 代表通用地址类型,这里的参数my_addr 的类型取决于socket(2) 中type参数指定的通讯类型。调用bind(2)时,要再用struct sockaddr 强制类型转化一下my_addr。
- 比如socket(2)的type是SOCK_STREAM, bind(2) 中的 my_addr 的类型就是struck sockaddr_in
3. listen(2)
- 收到但没处理完的也称为未决连接
3. accept(2)
- addrlen 是一个值结果参数,有可能会改变(就是accept执行完以后,addrlen可能会变)
4. connect(2)
5. struct sockaddr
- 代表通用地址类型,这是一个通用的方式,具体类型依赖于地址家族,比如全网通手机的通信服务商取决于SIM卡的类型。所以都是先用具体的协议家族,然后再转为通用的地址类型。
- ad_data的长度不一定是14
- 下面讲下具体的协议地址家族
3.4 地址家族
1. ipv4
- 第一个值确定AF_INET ,它代表IPV4
- 用到ipv4地址家族,需要包含头文件 #include <netinet/in.h>
$ man 7 ip
#include <sys/socket.h>
#include <netinet/in.h> # 这里里面定义了IPV4的地址家族
#include <netinet/ip.h>
....
# 在bind调用中, 当INADDR_ANY被指定时,socket将绑定所有的本地接口
When INADDR_ANY is specified in the bind call, the socket will be bound to all local interfaces. .
- sin_port是端口号,是网络字节序的 ,网络字节序一律采用大端的方式
- 这里需要用htons(2) 把主机字节序转为网络字节序 ,才能用
- htons(2) 是将整型变量从主机字节顺序转变成网络字节顺序, 就是整数在地址空间存储方式变为高位字节存放在内存的低地址处。
- sin_addr是网络地址,是结构体
- 里面的addr是网络字节序的,无符号32位的整型(我们一般看到的是点分十进制的表示方式, 需要用inet_pton(2) 把点分十进制转换为无符号32位的整型)
IPV6协议地址家族
- 第一个取值固定,AF_INET6
3.6. 网络字节序和主机字节序的转换
3.7 inet_pton(2) 和 inet_ntop(2)
- 文本格式转换二进制格式
- 127.0.0.1 -------> 1111111 00000000 00000000 0000001
4. 代码示例
- 服务端将客户端请求的字符串转为大写,打印客户端IP,并回应客户端
- 客户端向服务端发送字符串,并将服务端返回的结果打印出来。
- 并且是一次连接,多次交互的。
server.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <string.h>
#define E_MSG(STRING, VAL) do{perror(STRING); return(VAL);}while(0)
int main(void){
// 从连接描述符中读取到的数据放到buf中
char buf[128];
// IP放客户端的IP
char IP[32];
// 定义并初始化地址,地址后面bind(2) 要用
struct sockaddr_in serv, cli;
socklen_t cli_len;
// 创建一个通讯端点,返回该断点的文件描述符
int s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd == -1)E_MSG("socker", -1);
// 对serv变量的成员进行初始化,sin_family 要和socket(2)的domain参数类型对应上。主机字节序端口号需要转为网络字节序
serv.sin_family = AF_INET;
serv.sin_port=htons(55579);
// 绑定地址时,本机可能会有多个ip,服务器端需要监听所有的IP地址
serv.sin_addr.s_addr = htons(INADDR_ANY);
// 将s_fd和本地地址,端口号进行绑定,把ipv4类型的地址转为通用类型
int b = bind(s_fd, (struct sockaddr *)&serv, sizeof(serv));
if(b == -1)E_MSG("bind", -1);
// 将s_fd 设置为被动连接,监听客户端连接的到来,并放入未决连接队列中
// 指定未决连接队列的长度
listen(s_fd, 5);
while (1)
{
cli_len = sizeof(cli);
// 从s_fd 设备的未决连接队列中提取一个进行处理,并返回连接描述符
// 使用这个连接描述符和客户端进行通讯
// &cli_len 是值结果参数,有可能会被改变,结果是cli结构体的长度
int c_fd = accept(s_fd, (struct sockaddr *)&cli, &cli_len);
if(c_fd == -1)E_MSG("accept", -1);
// 到这里,三次握手已经ok了,就可以进行数据的传输了
// 打印下客户端的IP, binary --> text
inet_ntop(AF_INET, &cli.sin_addr, IP, 32);
printf("client IP: %s\n", IP);
while(1){
// 从c_fd中读取客户端发送过来的请求信息,一次只能处理128个字节,阻塞等客户端请求
int r = read(c_fd, buf, 128);
// 处理客户端请求的信息
for (int i=0; i < r; i++){
buf[i] = toupper(buf[i]);
}
// 将结果送回客户端
write(c_fd, buf, r);
if(strcmp(buf, "EXIT")==0)break;
}
// 关闭本次连接
close(c_fd);
}
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#define E_MSG(STRING, VAL) do{perror(STRING); return(VAL);}while(0)
int main(int argc, char *argv[]){
char buf[128]; // 接受服务端传来的数据
// char *msg = "this is a test ....\n";
char msg[128];
struct sockaddr_in serv; //定义初服务器地址变量
// 创建socket设备,返回设备的文件描述符
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)E_MSG("socket", -1);
// 服务器的信息初始化
serv.sin_family = AF_INET;
serv.sin_port=htons(55579);
// text ---> binary, 结果放在serv.sin_addr
inet_pton(AF_INET, argv[1], &serv.sin_addr);
// 使用fd向服务器发起连接
int c = connect(fd, (struct sockaddr *)&serv, sizeof(serv));
if(c==-1)E_MSG("connect", -1);
//到这里三次握手就成功了
while(1){
gets(msg, 20);//会把回车转为'/0'
// 向服务器发送请求信息, strlen(3) 返回的长度不带'\0'
write(fd, msg, strlen(msg) + 1);
// 阻塞等待服务器的响应
int r = read(fd, buf, 128);
if(strcmp(buf, "EXIT")==0)break;
// 将相应信息输出到显示器
write(1, buf, r);
printf("\n");
}
// 关闭文件描述符
close(fd);
return 0;
}
- 下图右边最后一行客户端吧再去访问服务端,服务端没有回应,因为还在上一个循环中没有出来,这次请求会被放入未决连接队列中。
- 下图右边第一次访问中,在最后输入 exit ,服务端收到以后就会退出这次连接,accpet 阻塞等待下一个连接到来.
- 这样就每次只能处理一个客户端的请求,不好,改进一下。参见:TCP并发服务器的实现
4. bind: Address already in use
- 有时会遇到 bind: Address already in use 错误,这是因为内核中的地址和端口号还没有释放,得等一会。可以用setsockopt(2) 来解决。
5. 封装
- 这篇TCP代码封装会把以上代码封装起来。