27TCP
TCP通信流程步骤:
服务端: 等待(被动)接收发送
1: 创建 socket: socket()
2: 绑定端口: bind()
3: 监听端口: listen()
4: 接受连接: accept()
5: 读取消息: read()
6: 发送消息: write()
7: 关闭套接字: close()
客户端:主动发送接收
1: 创建 socket: socket()
2: 连接服务端: connect()
3: 发送数据: write()
4: 接受结果: read()
5: 关闭套接字: close()
TCP通信流程图:
什么是套接字?
简单点,就是IP地址+端口号。
注意:IP地址决定往哪个主机发送,端口决定哪个程序接受
优点:
1.像操作文件描述符一样操作套接字
2.双向通信接口,起源于管道
3.比管道功能更强,应用更广泛
4.支持 read , write 等操作用于收发数据
服务端具体函数解析:
创建套接字
#include<sys/socket.h>
int socket(int domain, int type, int protocol)
返回值: 套接字
参数:
domain 协议族
type 套接字类型
protocol 套接字协议
第一参数参考值:
socket 创建套接字 --- domain
PF_UNIX, PF_LOCAL 本地通信
PF_INET IPV4协议
PF_INET6 IPV6协议
PF_IPX Novell 公司的 IPC 通信协议
PF_NETLINK 与内核间的接口
PF_X25 ITU-T X.25 / ISO-8208
PF_AX25 无线 AX.25 协议
PF_ATMPVC 访问原始ATM的PVC(永久虚连接)
PF_APPLETALK 苹果公司的 Appletalk 协议
PF_PACKET 底层包接口
第二参数参考值:
socket 创建套接字 --- type
SOCK_STREAM 提供面向连接的,有序的,可靠数据流 TCP
SOCK_DGRAM 支持数据报 UDP
SOCK_SEQPACKET 提供基于连接的有序的,可靠数据报通信
SOCK_RAW 对原始网络协议访问
SOCK_RDM 提供可靠的数据报层,不保证有序性
SOCK_PATET 已废弃
注意:
套接字 SOCKET 类型
SOCK_STREAM
流套接字:使用TCP协议。提供面向连接的,有序的,可靠的数据通信流。如 telnet, http 等
SOCK_DGRAM
数据报套接字:使用 UDP 协议。 提供无连接的,无序的,不保证可靠性的数据通信流。如 tftp, bootp 等
SOCK_RAW
原始流套接字:收发原始数据包,应用于底层协议开发,进行底层操作
只有 root 用户才有权限创建这个 socket
第三参数参考值:
socket 创建套接字 --- protocol
<netinet/in.h>
IPPROTO_IP (0): 接受所有IP数据包
IPPROTO_ICMP: ICMP 协议 (ping)
IPPROTO_TCP: TCP 协议
IPPROTO_UDP: UDP协议
IPPROTO_RAW: RAW 只能发送包,且需要自己填写IP头,计算校验和
绑定套接字
int bind(int sockfd, struct sockaddr *myAddr, socklen_t addrLen)
sockfd 套接字描述符
myAddr 主机地址
addrLen sockaddr 结构体大小
实现本机地址(协议族+IP+端口) 与 套接字绑定,收发消息即读写套接字文件描述符
sockaddr 结构体:
套接字地址
struct sockaddr
{
u_short sa_family; //协议族
char sa_data[14]; //协议地址
}
对于不同的协议族,协议地址 sa_data[14] 有不同的描述方式
AF_INET 协议族的协议地址
struct sockaddr_in
{
short sin_family; /*地址族 AF_INET*/
u_short sin_port; /*端口号*/
struct in_addr sin_addr; /*32位IP地址*/
char sin_zero[8]; /*预留*/
}
注意:一般定义sockaddr_in,填写信息,再将sockaddr_in转成sockaddr再使用在bind函数。
IP地址转换
#include<arpa/inet.h>
ulong inet_addr(char* pAddr)
字符串 IP 转 整数 IP
int inet_aton(char* pAddr, struct in_addr *pInAddr)
字符串 IP 转 整数 IP
char* inet_ntoa(struct in_addr inAddr)
整数 IP 转 字符串 IP
字节序转换
网络字节序: 大字节序
主机字节序:
x86: 小字节序
ppc: 大字节序
主机序转网络序
ulong htonl(ulong host)
ushort htons(ushort host)
网络序转主机序
ulong ntohl(ulong net)
ushort ntohs(ushort net)
监听端口
int listen(int sockfd, int backlog)
sockfd 监听的套接字
backlog 套接字接收的最大连接数,超出则向客户端发出 ECONNEREFUSED 错误
连接处理
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
阻塞,等待接收客户端连接申请
接收成功,则创建套接字,用于发送消息给客户端
addr 获取到客户端地址
addrlen 获取到客户端地址大小
sockfd设置为非阻塞的情况,未接收到连接申请,则返回错误
关闭套接字
1: close(sockfd)
同文件操作
关闭时,只是将套接字访问计数器 -1, 计数器为0时真正关闭
用于创建子进程进行并发管理
2:shutdown(sockfd, how) 按需关闭套接字
SHUT_RD (0): 关闭读功能,丢弃接收到的数据
SHUT_WR(1): 关闭写功能,不能发送数据
SHUT_RDWR(2): 彻底关闭套接字连接
服务端具体函数解析:
-----------------------创建套接字与服务端一样,参考上面--------------
int connect(int sockfd, struct sockaddr *srvaddr, int addrlen)
sockfd 创建的用于通信套接字
srvaddr 接收端地址
addrlen 地址结构体大小
发送数据
write(int sockfd, void *buf, size_t len)
send(int sockfd, void *msg, int len, int flags)
flags 标志一般填0, 特殊情况下用法如下
MSG_CONFIRM
通知链路层,即将收到回应,链路层未收到回应,则会定期探测邻居消息 (只能用于 SOCK_DGRAM 和 SOCK_RAW)
MSG_DONTROUTE
不通过网关发送数据,只发送到同一子网计算机
MSG_DONTWAIT
使用非阻塞操作,阻塞则返回EAGAIN错误
MSG_EOR
结束记录(SOCK_SEQPACKET 时使用)
MSG_MORE
还有后续数据要发送,效果相当于套接字使用了TCP_CORK属性。通知内核,这个帧的数据还没发完,后续数据发送后,这个数据才能发送出去。
MSG_OOB
发送带外数据。提高优先级,先于其他数据进行发送
接收消息
read(int sockfd, void *buf, size_t len)
recv(int sockfd, void *buf, size_t len, int flags)
flags标志一般填0, 特殊情况下用法如下
MSG_DONTWAIT
非阻塞操作,阻塞则返回EAGAIN错误
MSG_OOB
接收带外数据
MSG_PEEK
只查看消息,不从缓冲区删除数据
MSG_TRUNC
返回包的真实长度(只用于流套接字)
MSG_WAITALL
等待接收到的数据长度为len后才返回
例子:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define SRV_PORT 8888
#define CLT_PORT 6666
void Tcp_server()
{
int fd;
int iRet;
struct sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
//创建套接字
fd = socket(PF_INET, SOCK_STREAM, 0);//IPPROTO_IP
if (fd < 0)
{
perror("Socket failed!");
return;
}
//协议、端口、ip地址等属性
addr.sin_family = AF_INET;//use IPV4 address
addr.sin_port = htons(SRV_PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY=0L;
//把套接字和IP、端口号进行绑定
iRet = bind(fd, (struct sockaddr*)&addr, addrlen);
if (iRet)
{
perror("Bind failed!");
close(fd);
return;
}
//同时监听一个套接字
iRet = listen(fd, 1);
if (iRet)
{
perror("Listen failed!");
close(fd);
return;
}
int clientfd;
struct sockaddr_in srcaddr;
char szTip[] = "Welcome!";
char szBuf[1024]={};
char szMsg[1024];
//接受信息,确认连通,再创建通信套接字,包含客户端的IP和端口号
clientfd = accept(fd, (struct sockaddr*)&srcaddr, &addrlen);
if (clientfd < 0)
{
perror("Accept failed!");
return ;
}
else
{
printf("Connect form %s[%d]\n", inet_ntoa(srcaddr.sin_addr), ntohs(srcaddr.sin_port));
//send(clientfd, szTip, strlen(szTip), 0);
write(clientfd, szTip, strlen(szTip)); //send data to client
while(1)
{
//接受
memset(szBuf, 0, sizeof(szBuf));
//iRet = recv(clientfd, szBuf, 1024, 0);
iRet = read(clientfd, szBuf, sizeof(szBuf));
if (iRet < 0)
{
perror("Fail to read!");
break;
}
printf("\nRecv: %s", szBuf);
//发送
fprintf(stderr, "Send:");
memset(szMsg, 0, sizeof(szMsg));
read(STDIN_FILENO, szMsg, sizeof(szMsg));
write(clientfd, szMsg, strlen(szMsg));
}
}
close(clientfd);
close(fd);
return ;
}
void Tcp_client()
{
char szDestIp[16];
int port;
fprintf(stderr, "Connect to:");
scanf("%s%d", szDestIp, &port);
int fd;
int iRet;
struct sockaddr_in srvaddr;
socklen_t addrlen = sizeof(srvaddr);
fd = socket(PF_INET, SOCK_STREAM, 0);
if (fd < 0)
{
perror("Socket failed!");
return;
}
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons((short)port);
srvaddr.sin_addr.s_addr = inet_addr(szDestIp);
iRet = connect(fd, (struct sockaddr*)&srvaddr, addrlen);
if (iRet)
{
perror("Connect failed!");
return;
}
char szBuf[1024];
char szMsg[1024];
while(1)
{
memset(szBuf, 0, sizeof(szBuf));
iRet = read(fd, szBuf, sizeof(szBuf));//receive message
if (iRet < 0)
{
perror("Read failed!");
break;
}
printf("Recv: %s\n", szBuf);
fprintf(stderr, "Send:");
memset(szMsg, 0, sizeof(szMsg));
read(STDIN_FILENO, szMsg, sizeof(szMsg));//get message
write(fd, szMsg, strlen(szMsg)); //send message
}
close(fd);
}
int main(int argc, char** argv)
{
if (argc!=2
|| (strcmp(argv[1], "s") && strcmp(argv[1], "c"))
)
{
printf("Usage: %s [ c | s ]\n", argv[0]);
printf("\t c : For start tcp client\n");
printf("\t s : For start tcp server\n");
return 0;
}
if (argv[1][0] == 's')
{
Tcp_server();
}
else if (argv[1][0] == 'c')
{
Tcp_client();
}
return 0;
}
TCP搜索端口:
搜素端口与ping的区别:
ping只能确认某IP地址是否存在,当知道IP地址,不知道端口号也无法进行通信。