一、tcp客户端
1. 工作流程
在整个流程中,主要涉及四个接口
socket() : 创建套接字,使用的套接字类型为流式套接字
connect() : 连接服务器
send() : 数据发送
recv() : 数据接收
2. 创建套接字
socket函数
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int socket(int domain,int type,int protocol)
函数功能
创建套接字
函数参数
domain: 协议族,AF_INTE
type:套接字类型
SOCK_STREAM:流式套接字,传输层使用tcp协议
SOCK_DGRAM:数据包套接字,传输层使用udp协议、
protocol:协议,可以填0
函数返回值
成功:返回套接字文件描述符
失败:返回-1,并设置errno
代码
#include <sys/types.h>
#include <sys/socket.h>
int main()
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
return 0;
}
3. 连接服务器
连接服务器要调用的函数为connect
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
函数功能
发起对套接字的连接(基于面向连接的协议)
函数参数
sockfd:套接字文件描述符
addr:连接的套接字的地址结构对象的地址(一般为服务器)
addrlen:地址结构的长度
函数返回值
成功:返回 0
失败:返回-1,并设置errno
代码
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 与服务器进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = connect(sockfd,(const struct sockaddr*)&addr,sizeof(struct
sockaddr_in));
if(ret==-1 )
{
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
else
{
printf("connect successful.\n");
}
close(sockfd);
return 0;
}
最后使用网络调试助手进行测试 .
二, TCP客户端发送与接收数据
2.1、发送数据
基于socket发送数据需要调用send函数
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t send(int sockfd,const void *buf,size_t len,int flags)
函数功能
send()调用只能在套接字处于连接状态时使用,用于向另一个套接字传输消息。
send(sockfd, buf, len, flags);
等价于
sendto(sockfd, buf, len, flags, NULL, 0);
函数参数
sockfd:套接字文件描述符
buf:发送缓冲区的地址
len:发送数据的长度
flags:发送标志位
函数返回值
成功:返回成功发送的字节数
失败:返回-1,并设置errno
代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 与服务器进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = connect(sockfd,(const struct sockaddr*)&addr,sizeof(struct
sockaddr_in));
if(ret==-1 )
{
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
// 向服务器端发送数据
char buf[]={"hello tcp!"};
ret = send(sockfd,buf,strlen(buf),0);
if(ret == -1)
{
perror("send failed.");
exit(EXIT_FAILURE);
}
close(sockfd);
return 0;
}
使用网络调试助手进行测试
2、接收数据
基于socket接收数据需要调用recv 函数
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
ssize_t recv(int sockfd,void *buf,size_t len,int flags)
函数功能
用于接收来自套接字的消息
recv()和read(2)之间的唯一区别是是否存在标志。对于零标志参数,recv()通常等同于read(2)
recv(sockfd, buf, len, flags);
等价于
recvfrom(sockfd, buf, len, flags, NULL, NULL);
函数参数
sockfd:套接字文件描述符
buf:接收缓冲区的地址
len:接收数据最大长度
flags:标志位
函数返回值
成功:返回成功接收的字节数
失败:返回-1,并设置errno
注意点:当流套接字对等体执行有序关闭时,返回值将为0
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 与服务器进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = connect(sockfd,(const struct sockaddr*)&addr,sizeof(struct
sockaddr_in));
if(ret==-1 )
{
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
// 从服务器端接收数据
char buf[256]={0};
ret = recv(sockfd,buf,sizeof(buf),0);
if(ret == -1)
{
perror("recv failed.");
exit(EXIT_FAILURE);
}
else if(ret == 0)
{
printf("stream socket shutdowm.");
}
else
{
printf("message:%s\n",buf);
}
close(sockfd);
return 0;
}
使用网络助手进行测试
三, TCP 服务器端实现流程
1、tcp服务器端实现流程
在上述流程中,相对于客户端主要增加以下新的流程
bind : 绑定
ip
地址与端口号,用于客户端连接服务器
listen : 建立监听队列,并设置套接字的状态为
listen
状态,表示可以接收连接请求
accept : 接受连接,建立三次握手,并创建新的文件描述符,用于数据传输
监听队列:

socket
套接字状态:

CLOSED : 关闭状态
SYN-SENT : 套接字正在试图主动建立连接
[
发送
SYN
后还没有收到
ACK]
,很短暂
SYN-RECEIVE : 正在处于连接的初始同步状态
[
收到对方的
SYN
,但还没收到自己发过去的
SYN
的ACK]
ESTABLISHED : 连接已建立
2、创建socket套接字、绑定IP+端口
绑定ip地址与端口号需要调用bind函数
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
函数功能
当使用socket创建套接字时,它存在于名称空间(地址族)中,但没有为其分配地址。Bind()将addr
指定的地址分配给文件描述符sockfd引用的套接字。传统上,此操作称为“为套接字分配名称”。
函数参数
sockfd:套接字文件描述符
addr:绑定的地址(ip+端口)
addrlen:以字节为单位指定addr所指向的地址结构的大小
返回值
成功:0
失败:-1,并设置errno
// 由于struct sockaddr结构体的ip和port设置不方便,因此常用下面的结构体
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
}
// 创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed.");
exit(EXIT_FAILURE);
}
// 绑定ip+端口号
struct sockaddr_in addr;
int addrlen = sizeof(struct sockaddr_in);
bzero(&addr,addrlen);
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(const struct sockaddr*)&addr,addrlen);
if(ret == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
return 0;
}
3、建立监听队列
在服务器绑定
ip
地址与端口号之后,则需要让
,这里需要服务器socket套接字设置为被动接受状,
这里需要调用listen
函数
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int listen(int sockfd,int backlog)
函数功能
设置套接字状态为被动监听,并创建监听队列
函数参数
sockfd:套接字文件描述符
backlog:监听队列的的最大长度。如果一个连接请求在队列已满时到达,客户机可能会收到一个带有
ECONNREFUSED指示的错误,如果底层协议支持重传,则该请求可能会被忽略,以便稍后重试连接成功。
函数返回值
成功:返回0
失败:返回-1,并设置errno
4、建立连接
当客户端发出连接请求之后,则需要调用accept函数建立连接
函数头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen)
函数功能
accept基于连接的套接字类型(SOCK_STREAM, SOCK_SEQPACKET)。它从监听套接字sockfd的挂起连接
队列中提取第一个连接请求,创建一个新的连接套接字,并返回一个引用该套接字的新文件描述符,新创
建的套接字未处于监听状态。原始套接字sockfd不受此调用的影响。
函数参数
sockfd:套接字文件描述符
addr:网络地址结构的指针(用于保存发送请求端的地址信息)
addrlen:网络地址结构长度的指针(结果参数:调用者必须初始化它以包含addr所指向的结构的大小)
函数返回值
成功:返回新的文件描述符
失败:-1,并设置errno
示例代码:创建一个服务器端程序,并与客户端建立连接,并打印客户端的
ip
地址和端口号
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#define BACKLOG 20
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
}
// 创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed.");
exit(EXIT_FAILURE);
}
// 绑定ip+端口号
struct sockaddr_in addr;
int addrlen = sizeof(struct sockaddr_in);
bzero(&addr,addrlen);
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(const struct sockaddr*)&addr,addrlen);
if(ret == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
// 建立监听队列
ret = listen(sockfd,BACKLOG);
if(ret == -1)
{
perror("listen failed");
exit(EXIT_FAILURE);
}
//建立连接
struct sockaddr_in client_addr;
int client_addrlen = sizeof(struct sockaddr_in);
int cfd = accept(sockfd,(struct sockaddr*)&client_addr,&client_addrlen);
if(cfd == -1)
{
perror("accept failed.");
exit(EXIT_FAILURE);
}
// 打印客户端的信息
printf("[client]
ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
return 0;
}
使用之前写的客户端进行通信
5、数据的发送和接收
示例代码:实现echo服务器
服务器代码 :
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#define BACKLOG 20
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
}
// 创建socket
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket failed.");
exit(EXIT_FAILURE);
}
// 绑定ip+端口号
struct sockaddr_in addr;
int addrlen = sizeof(struct sockaddr_in);
bzero(&addr,addrlen);
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(const struct sockaddr*)&addr,addrlen);
if(ret == -1)
{
perror("bind failed");
exit(EXIT_FAILURE);
}
// 建立监听队列
ret = listen(sockfd,BACKLOG);
if(ret == -1)
{
perror("listen failed");
exit(EXIT_FAILURE);
}
//建立连接
struct sockaddr_in client_addr;
int client_addrlen = sizeof(struct sockaddr_in);
int cfd = accept(sockfd,(struct sockaddr*)&client_addr,&client_addrlen);
if(cfd == -1)
{
perror("accept failed.");
exit(EXIT_FAILURE);
}
// 打印客户端的信息
//printf("[client]
ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
ssize_t rbytes= 0,sbytes=0;
char buf[256]={0};
while(1)
{
memset(buf,0,sizeof(buf));
// 接收客户端的消息(注意:使用新的文件标识符)
rbytes = recv(cfd,buf,sizeof(buf),0);
if(rbytes==-1)
{
perror("server recv failed.");
exit(EXIT_FAILURE);
}
else if(rbytes == 0)
{
fprintf(stderr,"stream socket shutdown.\n");
break;
}
else
{
printf("recv message: %s\n",buf);
// 发送出去
sbytes = send(cfd,buf,strlen(buf),0);
if(sbytes == -1)
{
perror("server send failed.");
exit(EXIT_FAILURE);
}
}
}
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
// 创建socket套接字的
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
// 与服务器进行连接
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port=htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = connect(sockfd,(const struct sockaddr*)&addr,sizeof(struct
sockaddr_in));
if(ret==-1 )
{
perror("connect");
close(sockfd);
exit(EXIT_FAILURE);
}
while(1)
{
// 向服务器端发送数据
char buf[256]={0};
memset(buf,0,sizeof(buf));
// 将最后一个字符设置为\0
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]='\0';
ret = send(sockfd,buf,strlen(buf),0);
if(ret == -1)
{
perror("send failed.");
exit(EXIT_FAILURE);
}
// 接收服务器端的数据
char recvbuf[512]={0};
memset(recvbuf,0,sizeof(recvbuf));
ssize_t rbytes = recv(sockfd,recvbuf,sizeof(recvbuf),0);
if(rbytes == -1)
{
perror("client recv failed.");
exit(EXIT_FAILURE);
}
else if(rbytes == 0)
{
fprintf(stderr,"server shutdown\n");
break;
}
else
{
printf("server message: %s\n",recvbuf);
}
}
close(sockfd);
return 0;
}
进行测试
四, TCP并发服务器-多进程
1、基于多进程的并发服务器框架
并发服务器的设计思想:
每个客户端于服务器建立连接之后,服务器都会创建一个子进程和子线程进行数据处理
并发服务器的优势:
提高了服务器处理请求的效率
并发服务器框架

2, 基于多进程的并发服务器的实现
1. 当服务器与客户端建立连接后,则会创建进程
while(1)
{
//建立连接
struct sockaddr_in client_addr;
int client_addrlen = sizeof(struct sockaddr_in);
//bzero(void *s, size_t n) 从s所指向的位置开始擦除内存中n字节的数据
bzero(&client_addr,client_addrlen);
int cfd = accept(sockfd,(struct
sockaddr*)&client_addr,&client_addrlen);
if(cfd == -1)
{
perror("accept failed.");
exit(EXIT_FAILURE);
}
// 打印客户端的信息
printf("[client]
ip=%s,port=%d\n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
// 创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork failed");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
// 执行子进程
do_handle(cfd);
}
}
2.
实现客户端数据处理函数
,
并在子进程中执行
void do_handle(int cfd)
{
ssize_t rbytes= 0,sbytes=0;
char buf[256]={0};
while(1)
{
memset(buf,0,sizeof(buf));
// 接收客户端的消息(注意:使用新的文件标识符)
rbytes = recv(cfd,buf,sizeof(buf),0);
if(rbytes==-1)
{
perror("server recv failed.");
close(cfd);
exit(EXIT_FAILURE);
}
else if(rbytes == 0)
{
fprintf(stderr,"stream socket shutdown.\n");
break;
}
else
{
printf("recv message: %s\n",buf);
// 发送出去
sbytes = send(cfd,buf,strlen(buf),0);
if(sbytes == -1)
{
perror("server send failed.");
close(cfd);
exit(EXIT_FAILURE);
}
}
}
close(cfd);
exit(EXIT_SUCCESS);
}
3.
通过信号来处理僵死进程
void do_sighandler(int sig)
{
// 子进程进入僵死状态后,释放相关资源并返回
wait(NULL);
}
int main(int argc,char* argv[])
{
if(argc !=3)
{
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);
}
// 信号处理函数(子进程结束,会发SIGCHLD信号)
if(signal(SIGCHLD,do_sighandler) == SIG_ERR)
{
perror("signal failed.");
exit(EXIT_FAILURE);
}
......
}