TCP通信流程
套接字函数
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h> // 包含了前2个
>>>>>>>>>>>>>>>>>>>>服务器端<<<<<<<<<<<<<<<<<<<<
int socket(int domain, int type, int protocol);
// 创建一个套接字
- domain: 协议族,AF_INET/AF_INET6
- type: 通信协议类型,SOCK_STREAM/SOCK_DGRAM
- protocol: 具体协议,一般给0
- SOCK_STREAM 默认TCP
- SOCK_DGRAM 默认UDP
// 成功返回文件描述符,失败返回-1
=============================================
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 将fd和本地IP以及端口号绑定,命名socket
- sockfd: socket()得到的文件描述符
- addr: 需要绑定的socket地址,封装了IP和端口号
- addrlen: 第二个参数的大小
// 成功返回0,失败返回-1
=============================================
int listen(int sockfd, int backlog);
// 监听这个socket上的连接
- socket: 同上
- backlog: 未连接和已连接的和的最大值 // 不能超过/proc/sys/net/core/somaxconn 4096
// 成功返回0,失败返回-1
=============================================
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 接收客户端连接,阻塞
- sockfd: 同上
- addr: 传出参数,连接成功后客户端的地址信息
- addrlen: 指定第二个参数的内存大小
// 成功返回用于通信的文件描述符,失败返回-1
>>>>>>>>>>>>>>>>>>>>客户端<<<<<<<<<<<<<<<<<<<<
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// 客户端连接服务器
- sockfd: 用于通信的文件描述符
- addr: 客户端要连接的服务器的地址信息
- addrlen: 第二个参数的内存大小
// 成功返回0,失败返回-1
=============================================
ssize_t write(int fd, const void *buf, size_t count);
// 写数据
=============================================
ssize_t read(int fd, void *buf, size_t count);
// 读数据
服务器端
1. 创建一个用于监听的套接字
- 监听:监听客户端的连接
- 套接字:就是一个文件描述符
2. 将这个监听的文件描述符和本地的IP和端口绑定
3. 设置监听,监听的fd开始工作
4. 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字
5. 开始通信,接收数据,发送数据
6. 通信结束,断开连接
// TCP通信的服务器端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1. 创建用于监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// inet_pton(AF_INET, "10.37.62.58", &saddr.sin_addr.s_addr);点分十进制字符串转为网络字节序整数
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(9999); // 主机字节序端口转为网络字节序
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
// 3. 监听
ret = listen(lfd, 8);
// 4. 接收客户端连接
struct sockaddr_in clientaddr;
int clen = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr *)&clientaddr, &clen);
// 5. 输出客户端信息
char clientIP[16];
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, clientIP, sizeof(clientIP));
unsigned short clientPort = ntohs(clientaddr.sin_port);
printf("client IP: %s, client Port: %d\n", clientIP, clientPort);
// 获取客户端的数据
char recvBuf[1024] = {0};
while (1) {
int len = read(cfd, recvBuf, sizeof(recvBuf));
if (len == -1) { // 报错
perror("read");
exit(-1);
}
else if (len > 0) { // 输出接收到的数据
printf("receive data: %s\n", recvBuf);
}
else if (len == 0) { // 客户端断开连接
printf("client close\n");
break;
}
// 给客户端发送数据
char *data = "hello, I am server";
write(cfd, data, strlen(data) + 1);
}
close(cfd);
close(lfd);// 关闭文件描述符
return 0;
}
客户端
1. 创建一个用于通信的套接字
2. 连接服务器,需要指定连接的服务器的IP和端口
3. 连接成功,和服务器通信,接收数据,发送数据
4. 通信结束,断开连接
// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1. 创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
// 2. 连接服务器
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "10.37.62.58", &serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
// 3. 通信
char recvBuf[1024] = {0};
while (1) {
char *data = "hello, I am client";
write(fd, data, strlen(data) + 1);
sleep(1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if (len == -1) {
perror("read");
exit(-1);
}
else if (len > 0) {
printf("receive data: %s\n", recvBuf);
}
else if (len == 0) {// 服务器端断开连接
printf("server close\n");
break;
}
}
close(fd);// 关闭连接
return 0;
}
半连接
两台主机通信时,每个socket都有两个缓冲区,一个接受数据,一个发送数据,如果关闭其中一个缓冲区,比如关闭接受数据,就只能发送,称为半连接
#include <sys/socket.h>
int shutdown(int sockfd, int how);
- sockfd: 需要关闭的socket描述符
- how: 选择关闭方式
- SHUT_RD(0): 关闭读功能
- SHUT_WR(1): 关闭写功能
- SHUT_RDWR(2): 关闭读写功能
假设服务端传输一个文件,文件传输完后,如何告知客户端文件已经传输完毕,可以在文件末尾添加EOF
,调用close
可以发送EOF
,但这样也会关闭服务器的输入端,假设此时客户端还要发送数据,就接收不到了,所以采用半关闭,关闭发送端,这样既能发送EOF
,又能继续接收数据
端口复用
- 防止服务器重启时之前绑定的端口还未释放
- 程序突然退出而系统没有释放端口
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd: 要操作的文件描述符
- level: SOL_SOCKET(端口复用的级别)
- optname:
- SO_REUSEADDR
- SO_REUSEPORT
- optval: 端口复用的值,1可以复用,0不可复用
- optlen: optval参数的大小
// 成功返回0,失败返回-1
设置端口复用要在服务器绑定端口之前
查看网络相关信息
netstat
-a: 查看所有socket
-p: 显示正在使用socket的程序名称
-n: 直接使用IP地址,不通过域名服务器
$ netstat -anp | grep 9999
tcp 0 0 0.0.0.0:9999 0.0.0.0:* LISTEN 24713/./server
tcp 0 0 127.0.0.1:9999 127.0.0.1:36012 ESTABLISHED 24713/./server
tcp 0 0 127.0.0.1:36012 127.0.0.1:9999 ESTABLISHED 24714/./client
断开服务器端
tcp 0 0 127.0.0.1:9999 127.0.0.1:36012 FIN_WAIT2 -
tcp 1 0 127.0.0.1:36012 127.0.0.1:9999 CLOSE_WAIT 24714/./client
说明经过了两次挥手,服务端处于FIN_WAIT_2
状态,客户端处于CLOSE_WAIT
状态
此时再结束客户端,客户端会发送FIN
,处于LAST_ACK
状态,服务端收到FIN
后回复ACK
,处于TIME_WAIT
状态,经过2MSL(Maximum Segment Life)
之后变成ClOSE
,释放端口,客户端收到ACK
之后变成CLOSE
tcp 0 0 127.0.0.1:9999 127.0.0.1:36012 TIME_WAIT -