TCP网络编程
TCP介绍
1、面向连接的流式协议,可靠,出错重传,且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接
TCP与UDP差异
TCP和UDP流程对比图
TCP编程流程
- 服务器
- 创建套接字socket()
- 将套接字与服务器网络信息结构体绑定bind()
- 将套接字设置为监听状态listen()
- 阻塞等待客户端的连接请求accept()
- 进行通信recv()、send()
- 关闭套接字close()
- 客户端
- 创建套接字socket()
- 发送客户端连接请求connect()
- 进行通信send()、recv()
- 关闭套接字close()
TCP socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
案例
#include <stdio.h> // printf
#include <sys/types.h> //
#include <sys/socket.h> // socket
#include <stdlib.h> // exit
int main(int argc, char *argv[])
{
int sockfd; // 文件描述符
// 创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("fail to socket");
exit(1);
}
printf("sockfd = %d\n", sockfd);
return 0;
}
TCP客户端
connect函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
注意:
1、connect建立连接后不会产生新的套接字
2、连接成功后才可以开始传输TCP数据
send函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
注意:
不能用TCP协议发送0长度的数据包
recv函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
客户端code
编写客户端代码,使用网络调试助手作为tcp服务端
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include<unistd.h> // close
#include <sys/types.h> //
#include <sys/socket.h> // socket
#include<arpa/inet.h> // htons inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/stat.h>
#define N 128
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
int sockfd; // 文件描述符
// 第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("fail to socket");
exit(1);
}
// printf("sockfd = %d\n", sockfd);
// 第二步:发送客户端连接请求
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
serveraddr.sin_family = AF_INET; // 协议族,AF_INET:ipv4网络协议
// atoi函数是将字符串转换成整数
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
serveraddr.sin_port = htons(atoi(argv[2])); // 端口号
if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
perror("fail to connect");
exit(1);
}
// 第三步:进行通信
// 发送数据
char buf[N] ="";
// while (1)
// {
fgets(buf, N, stdin);
buf[strlen(buf) - 1] = '\0'; // 把buf字符串中的\n转化为\0
if(send(sockfd, buf, N, 0) == -1)
{
perror("fail to send");
exit(1);
}
// }
// 接收数据
char text[N] = "";
if(recv(sockfd, text, N, 0) == -1)
{
perror("fail to recv");
exit(1);
}
// 打印接收到的数据
printf("from server: %s\n", text);
// 第四步:关闭套接字文件描述符
close(sockfd);
return 0;
}
TCP服务器-bind、listen、accept
对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始
bind函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
示例:
listen函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
accept函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>
int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
TCP服务器例子
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include<unistd.h> // close
#include <sys/types.h> //
#include <sys/socket.h> // socket
#include<arpa/inet.h> // htons inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/stat.h>
#define N 128
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
exit(1);
}
// 第一步:创建套接字
int sockfd; // 文件描述符
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("fail to socket");
exit(1);
}
// printf("sockfd = %d\n", sockfd);
// 第二步:将套接字与服务器网络信息结构体绑定
struct sockaddr_in serveraddr;
socklen_t addrlen = sizeof(serveraddr);
serveraddr.sin_family = AF_INET; // 协议族,AF_INET:ipv4网络协议
// atoi函数是将字符串转换成整数
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
serveraddr.sin_port = htons(atoi(argv[2])); // 端口号
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
{
perror("fail to bind");
exit(1);
}
// 第三步:将套接字设置为被动监听状态
if(listen(sockfd, 10) == -1)
{
perror("fail to listen");
exit(1);
}
// 第四步:阻塞等待客户端的链接请求
int acceptfd;
struct sockaddr_in clientaddr;
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
{
perror("fail to accept");
exit(1);
}
// 打印连接的客户端信息
printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 第五步:进行通信
// tcp服务器与客户端通信时,需要使用accept函数的返回值
// 接收数据
char text[N] = "";
if(recv(acceptfd, text, N, 0) == -1)
{
perror("fail to recv");
exit(1);
}
// 打印接收到的数据
printf("from client: %s\n", text);
// 发送数据
strcat(text, " *_*"); // strcat(函数) 将两个char类型连接
// while (1)
// {
if(send(acceptfd, text, N, 0) == -1)
{
perror("fail to send");
exit(1);
}
// }
// 关闭套接字文件描述符
close(acceptfd);
close(sockfd);
return 0;
}
执行结果:
close关闭套接字
1、使用close函数即可关闭套接字
关闭一个代表已连接套接字将导致另一端收到一个0长度的数据包
2、做服务器时
- 关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接
- 关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
3、做客户端时
关闭连接就是关闭连接,不意味着其他
三次握手
四次挥手
TCP并发
注意: TCP不能实现并发的原因
由于TCP服务器端有两个读阻塞函数,accept和recv,两个函数需要先后运行,所以导致运行一个函数的时候,另一个函数无法执行,所以无法保证一边连接客户端,一边与其他客户端通信。
如何实现TCP并发服务?
使用多进程或多线程的方式实现
多进程并发案例
多进程并发案例
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include<unistd.h> // close
#include <sys/types.h> //
#include <sys/socket.h> // socket
#include<arpa/inet.h> // htons inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/wait.h>
#include <signal.h>
#include <signal.h>
#include <sys/stat.h>
// 使用多进程实现TCP并发服务器
#define N 128
#define ERR_LOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)
void handler(int sig)
{
wait(NULL);
}
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd; // 文件描述符
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
// 第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
ERR_LOG("fail to socket");
}
// 将套接字设置为允许重复使用本地地址或者为设置为端口复用
int on = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
ERR_LOG("fail to setsockopt");
}
// 第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET; // 协议族,AF_INET:ipv4网络协议
// atoi函数是将字符串转换成整数
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
serveraddr.sin_port = htons(atoi(argv[2])); // 端口号
// 第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
ERR_LOG("fail to bind");
}
// 第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) < 0)
{
ERR_LOG("fail to listen");
}
// 使用信号,异步的方式处理僵尸进程
signal(SIGCHLD, handler);
while (1)
{
// 第五步:阻塞等待客户端的链接请求
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
{
ERR_LOG("fail to accept");
}
// 打印连接的客户端信息
printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 使用fork函数创建子进程,父进程继续负责连接,子进程负责与客户端通信
pid_t pid;
if((pid = fork()) < 0)
{
ERR_LOG("fail to fork");
}
else if(pid > 0) // 父进程负责执行accept,所以if语句结束后继续在accept函数的位置阻塞
{
}
else // 子进程负责跟指定的客户端通信
{
char buf[N] = "";
ssize_t bytes;
while (1)
{
if((bytes = recv(acceptfd, buf, N, 0)) < 0)
{
ERR_LOG("fail to recv");
}
else if(bytes == 0)
{
printf("The client quited\n");
exit(0);
}
if(strncmp(buf, "quit", 4) == 0)
{
exit(0);
}
// 打印接收到的数据
printf("from client: %s\n", buf);
strcat(buf, " ^_^"); // strcat(函数) 将两个char类型连接
if(send(acceptfd, buf, N, 0) < 0)
{
ERR_LOG("fail to send");
}
}
}
}
return 0;
}
多线程并发案例
多线程并发案例
#include <stdio.h> // printf
#include <stdlib.h> // exit
#include<unistd.h> // close
#include <sys/types.h> //
#include <sys/socket.h> // socket
#include<arpa/inet.h> // htons inet_addr
#include<netinet/in.h>
#include<string.h>
#include <signal.h>
#include <sys/stat.h>
#include<pthread.h>
// 使用多线程实现TCP并发服务器
#define N 128
#define ERR_LOG(errmsg) do{\
perror(errmsg);\
exit(1);\
}while(0)
typedef struct{
struct sockaddr_in addr;
int acceptfd;
}MSG;
void *pthread_fun(void *arg)
{
char buf[N] = "";
ssize_t bytes;
MSG msg = *(MSG *)arg;
while (1)
{
if((bytes = recv(msg.acceptfd, buf, N, 0)) < 0)
{
ERR_LOG("fail to recv");
}
else if(bytes == 0)
{
printf("The client quited\n");
pthread_exit(NULL);
}
if(strncmp(buf, "quit", 4) == 0)
{
printf("The client quited\n");
pthread_exit(NULL);
}
// 打印接收到的数据
printf("from client: %s\n", buf);
printf("[%s - %d]\n", inet_ntoa(msg.addr.sin_addr), ntohs(msg.addr.sin_port));
strcat(buf, " ^_^"); // strcat(函数) 将两个char类型连接
if(send(msg.acceptfd, buf, N, 0) < 0)
{
ERR_LOG("fail to send");
}
}
}
int main(int argc, char *argv[])
{
if(argc < 3)
{
fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
exit(1);
}
int sockfd, acceptfd; // 文件描述符
struct sockaddr_in serveraddr, clientaddr;
socklen_t addrlen = sizeof(serveraddr);
// 第一步:创建套接字
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
ERR_LOG("fail to socket");
}
// 将套接字设置为允许重复使用本地地址或者为设置为端口复用
int on = 1;
if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
ERR_LOG("fail to setsockopt");
}
// 第二步:填充服务器网络信息结构体
serveraddr.sin_family = AF_INET; // 协议族,AF_INET:ipv4网络协议
// atoi函数是将字符串转换成整数
serveraddr.sin_addr.s_addr = inet_addr(argv[1]); // ip地址
serveraddr.sin_port = htons(atoi(argv[2])); // 端口号
// 第三步:将套接字与服务器网络信息结构体绑定
if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
{
ERR_LOG("fail to bind");
}
// 第四步:将套接字设置为被动监听状态
if(listen(sockfd, 5) < 0)
{
ERR_LOG("fail to listen");
}
while (1)
{
// 第五步:阻塞等待客户端的链接请求
if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
{
ERR_LOG("fail to accept");
}
// 打印连接的客户端信息
// printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));
// 创建子线程与客户端进行通信
MSG msg;
msg.addr = clientaddr;
msg.acceptfd = acceptfd;
pthread_t thread;
if(pthread_create(&thread, NULL, pthread_fun, &msg) != 0)
{
ERR_LOG("fail to pthread_create");
}
pthread_detach(thread);
}
return 0;
}
执行结果:
[root@iZ5bg01fils2m1fw0td6qgZ xhl]# gcc a_1.c -lpthread