实现基于TCP的服务端/客户端
服务端套接字创建过程
第一步:调用socket函数创建套接字
//成功时返回文件表述符,失败时返回-1
int socket(int __domain, int __type, int __protocol)
- domain:套接字使用的协议族(Protocol Family)信息;
- type:套接字数据传输类型信息;
- protocol:计算机间通信中使用的协议信息;
协议族(Protocol Family)
Name Purpose Man page
AF_UNIX Local communication unix(7)
AF_LOCAL Synonym for AF_UNIX
AF_INET IPv4 Internet protocols ip(7)
AF_IPX IPX - Novell protocols
AF_INET6 IPv6 Internet protocols ipv6(7)
AF_PACKET Low-level packet interface packet(7)
套接字类型(Type):是指套接字的传输方式
- 面向连接的套接字(SOCK_STREAM)
- 传输过程中数据不会消失
- 按序传输数据
- 传输的数据不存在数据边界(Boundary):调用了三次write传递了100字节,接受者仅一次read接收了全部
- 面向消息的套接字(SOCK_DGRAM)
- 强调快速传输而非传输顺序
- 传输的数据可能丢失也可能损毁
- 传输的数据有数据边界
- 限制每次传输的数据大小
协议的最终选择
/* Standard well-defined IP protocols. */
enum
{
IPPROTO_IP = 0, /* Dummy protocol for TCP. */
#define IPPROTO_IP IPPROTO_IP
IPPROTO_ICMP = 1, /* Internet Control Message Protocol. */
#define IPPROTO_ICMP IPPROTO_ICMP
IPPROTO_IGMP = 2, /* Internet Group Management Protocol. */
#define IPPROTO_IGMP IPPROTO_IGMP
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94). */
#define IPPROTO_IPIP IPPROTO_IPIP
IPPROTO_TCP = 6, /* Transmission Control Protocol. */
#define IPPROTO_TCP IPPROTO_TCP
IPPROTO_EGP = 8, /* Exterior Gateway Protocol. */
#define IPPROTO_EGP IPPROTO_EGP
IPPROTO_PUP = 12, /* PUP protocol. */
#define IPPROTO_PUP IPPROTO_PUP
IPPROTO_UDP = 17, /* User Datagram Protocol. */
#define IPPROTO_UDP IPPROTO_UDP
IPPROTO_IDP = 22, /* XNS IDP protocol. */
#define IPPROTO_IDP IPPROTO_IDP
IPPROTO_TP = 29, /* SO Transport Protocol Class 4. */
#define IPPROTO_TP IPPROTO_TP
IPPROTO_DCCP = 33, /* Datagram Congestion Control Protocol. */
#define IPPROTO_DCCP IPPROTO_DCCP
IPPROTO_IPV6 = 41, /* IPv6 header. */
#define IPPROTO_IPV6 IPPROTO_IPV6
IPPROTO_RSVP = 46, /* Reservation Protocol. */
#define IPPROTO_RSVP IPPROTO_RSVP
IPPROTO_GRE = 47, /* General Routing Encapsulation. */
#define IPPROTO_GRE IPPROTO_GRE
IPPROTO_ESP = 50, /* encapsulating security payload. */
#define IPPROTO_ESP IPPROTO_ESP
IPPROTO_AH = 51, /* authentication header. */
#define IPPROTO_AH IPPROTO_AH
IPPROTO_MTP = 92, /* Multicast Transport Protocol. */
#define IPPROTO_MTP IPPROTO_MTP
IPPROTO_BEETPH = 94, /* IP option pseudo header for BEET. */
#define IPPROTO_BEETPH IPPROTO_BEETPH
IPPROTO_ENCAP = 98, /* Encapsulation Header. */
#define IPPROTO_ENCAP IPPROTO_ENCAP
IPPROTO_PIM = 103, /* Protocol Independent Multicast. */
#define IPPROTO_PIM IPPROTO_PIM
IPPROTO_COMP = 108, /* Compression Header Protocol. */
#define IPPROTO_COMP IPPROTO_COMP
IPPROTO_SCTP = 132, /* Stream Control Transmission Protocol. */
#define IPPROTO_SCTP IPPROTO_SCTP
IPPROTO_UDPLITE = 136, /* UDP-Lite protocol. */
#define IPPROTO_UDPLITE IPPROTO_UDPLITE
IPPROTO_MPLS = 137, /* MPLS in IP. */
#define IPPROTO_MPLS IPPROTO_MPLS
IPPROTO_RAW = 255, /* Raw IP packets. */
#define IPPROTO_RAW IPPROTO_RAW
IPPROTO_MAX
};
第二步:调用bind函数分配IP地址和端口号
//成功时返回0,失败时返回-1
int bind(int __fd, const struct sockaddr *__addr, socklen_t __len)
- __fd:要分配地址信息(IP地址和端口号)的套接字文件表述符。
- __addr:存有地址信息的结构体变量地址值
- __len:第二个结构体变量的长度
第三步:调用listen函数转为可接收请求状态
//成功时返回0,失败时返回-1
int listen(int __fd, int __n)
- __fd:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)。
- __n:连接请求等待队列的长度。
第四步:调用accept函数受理连接请求
//成功时返回0,失败时返回-1
int accept(int __fd, struct sockaddr *__restrict__ __addr, socklen_t *__restrict__ __addr_len)
- __fd:服务器套接字的文件描述符。
- __addr:保存发起连接请求的客户端地址信息地变量地址值,调用函数后向传递来的地址变量参数填充客户端的地址信息。
- __addr_len:第二个参数__addr结构体的长度,但是存有长度的变量地址。函数调用完后,该变量即被填入客户端地址长度。
客户端创建套接字过程
第一步:调用socket函数创建套接字
//成功时返回文件表述符,失败时返回-1
int socket(int __domain, int __type, int __protocol)
第二步:调用socket函数向服务器端发送连接请求
//成功时返回0,失败时返回-1
int connect(int __fd, const struct sockaddr *__addr, socklen_t __len)
- __fd:客户端套接字文件表述符。
- __addr:保存目标服务器端地址信息的变量地址值。
- __len:以字节为单位传递已传递给第二个结构体参数__addr地址变量长度。
客户端调用connect函数后服务端接收连接请求(记录到等待队列)或发生断网等异常情况而中断连接请求才会返回(完成函数调用)。客户端的IP地址和端口号在调用connect函数时由内核自动分配,无需调用标记的bind函数进行分配。
总体流程就是:服务器端创建套接字后联系调用bind、listen函数进入等待状态,客户端通过调用connect函数发起连接请求,需要注意的是,客户端只能等到服务器端调用listen函数后才能调用connect函数。同时要清楚,客户端调用connect前,服务器端可能先调用了accept函数。当然,此时服务器端在调用accept函数时进入了阻塞状态,直到客户端调用connect函数为止。
TCP套接字中的I/O缓冲
- I/O缓冲在每个TCP套接字中单独存在;
- I/O缓冲在创建套接字时自动生成;
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据;
- 关闭套接字将丢失输入缓冲中的数据;
编写
echosrv.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc, char** argv) {
// 1. 创建套接字
int listenfd;
//协议族 套接字类型 协议类型
if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
int on = 1;
// 确保time_wait状态下同一端口仍可使用
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on) < 0)
{
perror("setsockopt");
}
// 3. 绑定套接字地址
if (bind(listenfd, (struct sockaddr*) &servaddr, sizeof servaddr) < 0) {
perror("bind");
}
// 4. 等待连接请求状态
if (listen(listenfd, SOMAXCONN) < 0) {
perror("listen");
}
// 5. 允许连接
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof peeraddr;
int connfd;
if ((connfd = accept(listenfd, (struct sockaddr *) &peeraddr, &peerlen)) < 0) {
perror("accept");
}
printf("id = %s, ", inet_ntoa(peeraddr.sin_addr));
printf("port = %d\n", ntohs(peeraddr.sin_port));
// 6. 数据交换
char recvbuf[1024];
while (1)
{
memset(recvbuf, 0, sizeof recvbuf);
int ret = read(connfd, recvbuf, sizeof recvbuf);
if (ret == 0)
{
printf("client close\n");
break;
} else if (ret == -1)
{
perror("read");
}
fputs(recvbuf, stdout);
write(connfd, recvbuf, ret);
}
// 7. 断开连接
close(connfd);
close(listenfd);
return 0;
}
echocli.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
// 1. 创建套接字
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror("socket");
}
// 2. 分配套接字地址
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof servaddr);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
// servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
// inet_aton("127.0.0.1", &servaddr.sin_addr);
// 3. 请求链接
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof servaddr) < 0) {
perror("connect");
}
// 4. 数据交换
char recvbuf[1024] = {0};
char sendbuf[1024] = {0};
while (fgets(sendbuf, sizeof sendbuf, stdin) != NULL) // 键盘输入获取
{
// memset(recvbuf, 0, sizeof recvbuf);
// memset(sendbuf, 0, sizeof sendbuf);
write(sockfd, sendbuf, sizeof sendbuf); // 写入服务器
int ret = read(sockfd, recvbuf, sizeof recvbuf); // 服务器读取
if (ret == 0)
{
printf("server close\n");
break;
} else if (ret == -1)
{
perror("read");
}
fputs(recvbuf, stdout); // 服务器返回数据输出
// 清空
memset(recvbuf, 0, sizeof recvbuf);
memset(sendbuf, 0, sizeof sendbuf);
}
// 5. 断开连接
close(sockfd);
return 0;
}