之前写过UDP的socket通讯,下面将介绍TCP的socket通讯,同时提供了TCP客户端与并发服务器端的几个案例。
目录
简介
socket开发API介绍
TCP并发服务器
简介
与UDP的区别
TCP |
UDP |
|
是否面向连接 |
? |
X |
是否可靠 |
? |
X |
支持广播、多播 |
X |
? |
效率 |
低 |
高 |
客户端与服务器TCP通讯流程
socket开发API介绍
1、创建tcp套接字socket()
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if ( sockfd < 0 )
{perror("socket");exit(-1);
}
开发TCP客户端需要满足以下几点:
1、知道服务器的ip、port;
2、需主动连接服务器;
3、使用函数:socket()、connect()、send()、recv()、close()
Note:tcp的一些函数没有目的指向,比如是 send() 而不是 sendto(),这是因为 tcp 客户端与服务器通讯之前是先建立连接后通讯,使用该函数时,已经知晓目的地址。
2、connect连接函数
//主动与服务器建立连接
#include <sys/socket.h>
int connect(int sockfd;const struct sockaddr *addr,socklen_t len
);
参数介绍sockfd : socketaddr : 连接的服务器地址结构len : 地址结构体长度
Return成功返回0
Noteconnect建立连接后不会产生新的套接字成功建立连接后才可以传输数据
3、send数据发送函数
//发送数据
#include <sys/socket.h>
ssize_t send(int sockfd,const void * buf,size_t nbtyes,int flags
);
参数sockfd :socketbuf :发送的数据nbytes :发送缓存数据大小flags :套接字标志(通常为0)
Return 成功发送的数据字节数
4、recv函数
//接收网络数据,阻塞式
#include <sys/socket.h>
ssize_t recv(int sockfd,void *buf,size_t nbytes,int flags
);
参数sockfd :socketbuf :接收网络数据的缓冲区nbytes :接收缓冲区的大小(以字节为单位)flags :套接字标志(通常为0)
Return成功接收的字节数
客户端发送与接受数据
以下代码将发送一次数据和接受数据,服务器可使用 NetAssist(windows)、linux下可使用自己开发的
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
int main(void)
{//create tcp socketint sockfd = socket(AF_INET,SOCK_STREAM,0);if (sockfd < 0){printf("CREATE SOCKET ERROR\n");return 0;}//bind是可选的, 固定ip和portstruct sockaddr_in client;bzero(&client,sizeof(client));client.sin_family = AF_INET;client.sin_port = htons(8000);client.sin_addr.s_addr = htonl(INADDR_ANY);bind(sockfd,,(struct sockaddr *)&client, sizeof(client));//connect 连接服务器struct sockaddr_in ser_addr;bzero(&ser_addr,sizeof(ser_addr));ser_addr.sin_port = htons(8080);ser_addr.sin_family = AF_INET;ser_addr.sin_addr.s_addr = inet_addr("192.168.31.96");connect(sockfd, (struct sockaddr *)&ser_addr,sizeof(ser_addr));//给服务器发送数据char buf[128] = "";printf("please enter the msg to send\n");fgets(buf,sizeof(buf),stdin);buf[strlen(buf) - 1] = 0;//消除回车的影响send(sockfd, buf, strlen(buf), 0);//接收数据char msg[128] = "";recv(sockfd, msg, sizeof(msg), 0);printf("received msg is %s\n",msg);close(sockfd);return 0;
}
开发服务器设计需要满足以下两点:
1、确定的地址和端口
2、监听(listen)与接收(accept)
设计流程:
1、socket()(主动)
2、bind() 固定ip和固定端口
3、listen()(主动变被动、创建连接队列)
4、accept() (从连接队列中提取成功建立的连接,接下来可以正式通讯)
根据上图TCP通讯流程图可以看出,服务器绑定固定端口,监听客户端的请求。客户端与服务器通讯前,要先与服务器端成功建立连接,需要经过三次握手。成功建立的连接会放入“完成链接”,服务器将使用accept()函数从该队列中取出客户端请求并处理。
1、listen()函数
//将套接字由主动改为被动
//操作系统为该套接字设置一个队列,用来记录所有链接到该套接字的连接
int listen(int sockfd, int backlog);
参数sockfd :socket监听套接字backlog :连接队列的长度
Return 成功返回 0
2、accept()函数
//从已建立队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen
);
参数sockfd :socketcliaddr :用于存放客户端socket的地址结构addrlen:套接字地址结构体长度的地址
Return 已连接的socket
TCP应用层无法发送长度为0的数据报,因为为0表示这通讯结束。
案例代码
可以上述client联合使用,客户端连接服务器的端口要与服务器端绑定的端口一致。
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
int main(void)
{//1、创建一个tcp监听套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);//2、使用bind函数给监听套接字绑定固定的ip和端口struct sockaddr_in server;bzero(&server, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(8080);server.sin_addr.s_addr = htonl(INADDR_ANY);bind(sockfd,(struct sockaddr *)&server, sizeof(server));//3、使用listen函数连接队列(主动变被动) listen(sockfd, 10); //4、使用accept函数从连接队列中提取已经完成的连接struct sockaddr_in client;bzero(&client,sizeof(client));socklen_t len = sizeof(client);//fd代表index客户端连接,client存储客户端的信息int fd = accept(sockfd, (struct sockaddr *)&client, &len);char ip[16] = "";inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);printf("client %s connected\n",ip);//5、获取客户端的请求,以及回应客户端char buf[128] = "";recv(fd, buf, sizeof(buf), 0);printf("client request is %s\n",buf);send(fd,"recv", strlen("recv"), 0);//6、关闭已连接套接字close(fd);//7、关闭监听套接字close(sockfd);return 0;
}
TCP并发服务器
可以同时服务多个客户端。TCP并发服务器本质还是TCP服务器,同时其又可以服务多个客户端。以下将提供两种方法:多进程与多线程
多线程
客户端正常退出不会有影响,但如果服务器端意外退出,再次运行bind的时候需要防止端口被占用(5~6分钟)。
#include <stdio.h>
#include <pthread.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <netinet/in.h>
#include <fcntl.h>
void *deal_client(void *sockfd);
int main(void)
{//1、创建TCP监听套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("CREATE SOCKET!!");}//端口复用int yes = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));//2、给socket绑定ip和端口信息struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(8088);server.sin_addr.s_addr = htonl(INADDR_ANY);int result = bind(sockfd, (struct sockaddr *)&server, sizeof(server));if (result == -1){perror("bind");}// 3、调用listen,将sockfd调整主动变为被动listen(sockfd, 10);//4、提取建立完成的链接//accept调用一次只能接待一个客户端,需要反复调用acceptwhile(1){struct sockaddr_in client;socklen_t len = sizeof(client);int new_fd = accept(sockfd, (struct sockaddr *)&client, &len);//遍历客户端信息char ip[16] = "";unsigned short port = ntohs(client.sin_port);inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);printf("client %s is connected %hu port\n", ip, port);//为每个客户端创建线程,单独的服务客户端pthread_t tid;pthread_create(&tid, NULL, deal_client, (void *)&new_fd);//线程分离pthread_detach(tid);}close(sockfd);return 0;
}
//TCP并发(并发服务器核心代码都不相同)
void *deal_client(void *sockfd)
{//通过arg获得已连接socket//给服务器发啥就回啥int fd = *(int *)sockfd;//服务器核心代码,处理客户端请求、while (1){char data[128] = "";int len = recv(fd, data, sizeof(data), 0);if (!len){break;}send(fd, data, len, 0);}close(fd);
}
多进程
父子进程、资源独立、某个进程结束不会影响已有进程,服务器更加稳定,代价是多进程会浪费资源
#include <stdio.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
void deal_client(int sockfd);
int main(void)
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket");}int yes;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));struct sockaddr_in server;server.sin_family = AF_INET;server.sin_addr.s_addr = htonl(INADDR_ANY);server.sin_port = htons(8088);bind(sockfd, (struct sockaddr *)&server, sizeof(server));listen(sockfd, 10);while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int sock_new = accept(sockfd, (struct sockaddr *)&client, &len);unsigned short port = ntohs(client.sin_port);char ip[16] = "";inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, 16);printf("client ip %s connected at port %hu\n",ip,port);//子进程//使用fork后,会复制父进程的全部状态资源,注意下面四个关闭socket,描述符if (fork() == 0){//关闭监听socket(进程中的,以防止与父进程冲突)close(sockfd);//服务于客户端的核心代码deal_client(sock_new);//关闭已连接socket(进程中的,以防止与父进程冲突)close(sock_new);_exit(-1);}else //父进程{//关闭父进程中的已连接socketclose(sock_new);}}//关闭父进程中的监听socketclose(sockfd);return 0;
}
void deal_client(int sockfd)
{while (1){char buf[128] = "";int len = recv(sockfd, buf, sizeof(buf), 0);if (!len){break;}//printf("the msg is %s\n",buf);send(sockfd, buf, len, 0);}
}