TCP 通信
这里的应用编写属于应用层的实现,而SOCKET的实现属于传输层
TCP鸟瞰图
socket socket
| |
| bind
| |
| lisent
connect-----------------|
| accept
| |
send recv
| |
close close
网络也是一种文件
socket文件对象实在内核态空间创建的,有两个缓冲区,分别是snd和rcv缓冲区,为了实现全双工通信,需要两个管道
各个api的作用
-
socket
创建一个套接字设备(规定地址类型例如ipv4、TCP流式传输和UDP数据报通信、默认socket协议)
-
bind
给套接字赋予一个本地地址,在TCP传输中,服务端必须要bind一个IP地址,因为客户端总是需要向服务端发送请求的,如果都绑定了地址,在断连后重连会浪费更多的时间,所以在tcp协议中,并不建议两端都bind,不bind本地ip地址会报错,bind 0.0.0.0 相当于绑定外网地址
- bind的流程
- 先创建struct sockaddr_in
- 设置好地址内容参数
- 传递参数
- bind的流程
-
connect
建立连接,要请求一个服务端的ip和端口,调用该api时,传输层会进行三次握手协议
-
listen
服务端开启监听,可以接受客户端的连接,执行listen之后,会复制一个socket文件对象,将snd和rcv缓冲区换成半连接队列和全连接队列,也就是说listen之后,socket将不再能够发送和接受数据,只能新建连接,处理三次握手相关的内容;所以我们把listen之后的socket称作服务端socket
-
accept
用于从服务端socket中的全连接队列中取出内容,构建一个新的文件对象(Net Socket),netsocket才是真正用于通信的
-
send
int send(int s, const void *msg, size_t len, int flags); ssize_t write(int fd, const void *buf, size_t count);
send是一个特殊的write函数
-
recv
ssize_t recv(int sockfd, void buf[.len], size_t len,int flags); ssize_t read(int fd, void *buf, size_t count);
recv是一个特殊的read函数
send/recv只是把数据在buf和select之间来回拷贝,真正的发送和接收行为是内核协议栈来完成的,如果服务端调用recv函数时,接收缓冲区是空的,则会阻塞;send/recv不是简单的收发关系,不是一一对应的,TCP数据是没有边界的,应用层的表现和两根管道是一模一样的
-
rcv缓冲区为空时,recv会阻塞
-
rcv非空时,recv的返回值在(0,count)
如果recv的ret为0,表示对面close了
-
即时通信系统
服务端:
int main(int argc, char* argv[])
{
// ./2_aqiang 192.168.68.128 1234
//socket 是两端通信的端点
ARGS_CHECK(argc, 3);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
ERROR_CHECK(ret, -1, "connect");
fd_set rdset;
char buf[4096];
while(1){
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sockfd, &rdset);
select(sockfd+1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset)){
//memset(buf, 0, sizeof(buf));
bzero(buf, sizeof(buf));
ssize_t sret = read(STDIN_FILENO, buf, sizeof(buf));
//send(sockfd, buf, strlen(buf), 0);
send(sockfd, buf, sret, 0);
}
if(FD_ISSET(sockfd, &rdset)){
bzero(buf, sizeof(buf));
ssize_t sret = recv(sockfd, buf, sizeof(buf), 0);
printf("buf = %s\n", buf);
}
}
return 0;
}
客户端
int main(int argc, char* argv[])
{
// ./2_aqiang 192.168.68.128 1234
//socket 是两端通信的端点
ARGS_CHECK(argc, 3);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
ERROR_CHECK(ret, -1, "connect");
fd_set rdset;
char buf[4096];
while(1){
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sockfd, &rdset);
select(sockfd+1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset)){
//memset(buf, 0, sizeof(buf));
bzero(buf, sizeof(buf));
ssize_t sret = read(STDIN_FILENO, buf, sizeof(buf));
//send(sockfd, buf, strlen(buf), 0);
send(sockfd, buf, sret, 0);
}
if(FD_ISSET(sockfd, &rdset)){
bzero(buf, sizeof(buf));
ssize_t sret = recv(sockfd, buf, sizeof(buf), 0);
printf("buf = %s\n", buf);
}
}
return 0;
}
聊天室
server
typedef struct conn_s{
int netfd;
int is_alive;
} conn_t;
int main(int argc, char* argv[])
{
// ./2_server 192.168.68.128 1234
ARGS_CHECK(argc, 3);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr; //服务端地址的结构体初始化
addr.sin_family = AF_INET;
//向ip结构体中传入ip地址和端口号参数
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
ERROR_CHECK(ret, -1, "bind");
listen(sockfd, 10); //DDOS攻击的点
conn_t clientArr[10]; //保存所有炼乳客户端的信息
int curidx = 0; //保存下一个炼乳客户端的下标
char buf[4096];
fd_set rdset;
fd_set monitorset;
FD_ZERO(&monitorset);
FD_SET(sockfd, &monitorset);
while(1){
memcpy(&rdset, &monitorset, sizeof(fd_set));
select(100, &rdset, NULL, NULL, NULL);
if(FD_ISSET(sockfd, &rdset)){
struct sockaddr_in clientAddr;
socklen_t clientAddrSize = sizeof(clientAddr); //该变量必须初始化
int netfd = accept(sockfd,(struct sockaddr *)&clientAddr, &clientAddrSize);
printf("netfd = %d\n", netfd);
printf("client ip = %s, port = %d\n",
inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));
clientArr[curidx].netfd = netfd;
clientArr[curidx].is_alive = 1; //1 表示存货 0表示已经断开
FD_SET(netfd, &monitorset);
++curidx;
}
for(int i = 0; i < curidx; ++i){
if(clientArr[i].is_alive && FD_ISSET(clientArr[i].netfd, &rdset)){
//读取该客户端发送的消息并且转发给其它所有的客户端
bzero(buf, sizeof(buf));
ssize_t sret = recv(clientArr[i].netfd, buf, sizeof(buf), 0);
if(sret == 0){
clientArr[i].isalive = 0; //i号客户端已经终止
FD_CLR(clientArr[i].netfd, &monitorset);
close(clientArr[i].netfd);
}
//转发给其他所有的客户端
for(int j = 0; j < curidx; ++j){
if(j == i || clientArr[j].is_alive == 0){
continue; //发送的目标是自己或者发送的目标已经关闭,就继续
}
//相当于做一个广播的操作
send(clientArr[j].netfd, buf, strlen(buf), 0);;
}
}
}
}
close(sockfd);
return 0;
}
client
int main(int argc, char* argv[])
{
// ./2_aqiang 192.168.68.128 1234
//socket 是两端通信的端点
ARGS_CHECK(argc, 3);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
ERROR_CHECK(ret, -1, "connect");
fd_set rdset;
char buf[4096];
while(1){
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sockfd, &rdset);
select(sockfd+1, &rdset, NULL, NULL, NULL);
if(FD_ISSET(STDIN_FILENO, &rdset)){
//memset(buf, 0, sizeof(buf));
bzero(buf, sizeof(buf));
ssize_t sret = read(STDIN_FILENO, buf, sizeof(buf));
//send(sockfd, buf, strlen(buf), 0);
send(sockfd, buf, sret, 0);
}
if(FD_ISSET(sockfd, &rdset)){
bzero(buf, sizeof(buf));
ssize_t sret = recv(sockfd, buf, sizeof(buf), 0);
printf("buf = %s\n", buf);
}
}
return 0;
}
杂项
-
端口号的选择:
0-65535之间进行选择,0-1000的端口属于悉知端口号,尽量不要选择
UDP通信
UDP鸟瞰图
客户端 服务端
socket socket
| |
| bind
| |
sendto -------> recvfrom
| |
recvfrom <------ sendto
|
close
问题?
TCP和UDP能否bind同一个端口?
可以bind,tcp和udp走的信道不同,不会产生干扰,因此可以bind同一个端口
UDP 需要listen 和 connect 吗?
不需要,UDP是无连接的
API功能概览
-
socket函数
int socket(int domain, int type, int protocal)
- domain 指定通讯选用的协议,常用的有IPv4网络协议,AF_INET
- type 用于指定通讯的语义,SOCK_STREAM 提供可靠有序的字节流通讯,而SOCK_DGRAM 提供数据报通讯
- protocal 用于指定套接字的协议
-
sendto 和 recvfrom 函数
ssize_t send(int sockfd, const void buf[.len], size_t len, int flags); ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recv(int sockfd, void buf[.len], size_t len, int flags); ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len, int flags, struct sockaddr *_Nullable restrict src_addr, socklen_t *_Nullable restrict addrlen);
UDP的 sendto 和 recvfrom 每次收发数据都需要绑定一个地址信息,而TCP不需要
-
UDP 必须客户端先 sendto , 服务端先 recvfrom,客户端的地址不是固定的,而服务端
UDP通信例子
一个简单的UDP通信例子:
-
服务端server
#include<func.h> int main(){ // ./4_server 0.0.0.0 1234 ARGS_CHECK(args, 3); int sockfd = socket(AF_INET, SOCK_DGRAM, 0); //UDP SOCK_DGRAM struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(atoi(argv[2])); serverAddr.sin_addr = inet_addr(argv[1]); // 进行bind操作 int ret = bind(sockfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); ERROR_CHECK(ret, -1, "bind"); // 服务端先recvfrom struct sockaddr_in clientAddr; socklen_t clientAddrSize = sizeof(clientAddr); char buf[4096]; recvfrom(sockfd, buf, sizeof(buf),0, (struct sockaddr *)&clientAddr, &clientAddrSize); printf("client ip = %s, port = %d\n", inent_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); printf("buf = %s", buf); close(sockfd); return 0; }
-
客户端
#include<func.h> int main(){ // ./4_client 192.168.68.128 1234 ARGS_CHECK(args, 3); int sockfd = socket(AF_INET, SOCK_DGRAM, 0); //UDP SOCK_DGRAM struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(atoi(argv[2])); serverAddr.sin_addr = inet_addr(argv[1]); // 客户端先 sendto sendto(sockfd, "zaima", 5, 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr)); return 0; }