TCP

1、TCP的概述
客户端:主动连接服务器、和服务器进行通信
服务器:被动被客户端连接,启动新的线程或进程 服务器客户端(并发服务器)

2、创建TCP套接字

int sockfd = socket(AF_INET, SOCK_STREAM, 0);

socket函数创建的TCP套接字,没有端口,默认为主动连接特性

3、调用connect函数连接服务器

tcp客户端通信之前 必须事先 建立和服务器的连接

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

addr地址结构体 存放的是服务器的IP、PORT

返回值:成功为0 失败-1


//连接服务器

struct sockaddr_in ser_addr;

bzero(&ser_addr, sizeof(ser_addr));

ser_addr.sin_family = AF_INET;

ser_addr.sin_port = htons(8000);

ser_addr.sin_addr.s_addr = inet_addr("10.9.11.251");

connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));

如果sockfd没有固定端口 在调用connect时系统自动分配随机端口为源端口

4、send发送消息


ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字
buf:需要发送的字符串的首元素地址
len:需要发送的字符串的实际长度
flags:默认为0

成功返回实际发送的字节数,失败返回-1
注意:TCP并不能发送0长度报文,但是UDP可以

5、recv接收消息(默认阻塞)


#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字
buf:存放收到的消息
len:最大能接收的长度
flags:默认为0

成功返回收到的实际字节数, 失败返回-1
recv如果收到0长度报文,表明对方已经断开连接。

close(sockfd);断开连接

6、tcp客户端 收发数据


#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
	//创建TCP套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd < 0)
	{
		perror("socket");
		return 0;

	}
	printf("sockfd = %d\n", sockfd);
	//连接服务器
	struct sockaddr_in ser_addr;
	bzero(&ser_addr, sizeof(ser_addr));
	ser_addr.sin_family = AF_INET;
	ser_addr.sin_port = htons(8000);
	ser_addr.sin_addr.s_addr = inet_addr("10.9.11.251");
	connect(sockfd, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
	//send发送数据
	send(sockfd, "hello tcp", strlen("hello tcp"), 0);
	//recv接收数据
	unsigned char buf[1500] = "";
	int len = recv(sockfd, buf, sizeof(buf), 0);
	printf("len=%d buf=%s\n", len, buf);
	//关闭套接字
	close(sockfd);
	return 0;

}

TCP服务器编程

1、作为服务器的条件
需要bind函数 为服务器绑定固定的端口、IP
使用listen函数 让服务器套接字 由主动变被动。
等待客户端的连接到来 accept提取到来的客户端。

2、listen 监听函数

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);

功能:
1、将sockfd由主动变被动,并且对sockfd进行监听客户端的连接到来
2、backlog代表的是连接队列的大小(客户端的个数)

3、accept提取客户端的连接(阻塞)
accept只能从连接队列中的完成部分 提取连接。

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

sockfd:监听套接字
addr:存放的是客户端的地址信息
addrlen:地址结构长度

返回值:
成功返回一个已连接套接字 代表服务器和该客户端的连接端点(真正和客户端的连
接)
失败:返回值-1

注意:调用一次 只能提取一个客户端对应的连接,如果连接队列没有完成的连接 将阻塞。


#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
//创建tcp套接字(如果是服务器 该套接字为监听套接字)
int lfd = socket(AF_INET, SOCK_STREAM, 0);
//bind绑定固定的IP、PORT
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(9000);
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (ret < 0)
{
perror("bind");
return 0;
}
//使用listen 对lfd进行监听客户端连接到来
listen(lfd, 10);
//提取客户端的连接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_len);
char ip_str[16] = "";
unsigned short port = 0;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, 16);
port = ntohs(client_addr.sin_port);
printf("连接:%s %hu到来了\n", ip_str, port);
//从已连接套接字cfd 获取客户端的请求
unsigned char buf[256] = "";
recv(cfd, buf, sizeof(buf), 0);
printf("客户端的请求:%s\n", buf);
//服务器应答客户端
send(cfd, "ok", 2, 0);
close(lfd);
close(cfd);
return 0;
}

close关闭套接字

1、作为客户端
close(套接字):断开当前的连接,导致服务器收到0长度 报文
2、作为服务器
close(监听套接字):该服务器 不能监听新的连接到来 但是不影响 已有连接
close(已连接套接字):只是断开与当前客户端的连接,不会影响监听套接字(服务器可以
继续监听新的连接到来)

三次握手

当客户端调用connect连接服务器时,底层会发起3次握手信号,当3次握手信号完成,
connect才会解阻塞往下执行。

SYN:置1 表示该报文是连接请求报文
FIN:置1 表示该报文是关闭连接请求报文
ACK:置1 表示当前为回应报文
紧急URG: 此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快
传送
PSH:推送报文
RST:复位连接
注意:
序号:当前报文的编号
确认序号:当前报文希望接下来 对方发送的报文编号

四次挥手

45是客户端,251是服务器

并发服务器--多进程版

并发服务器:服务器可以同时服务多个客户端

案例1:多进程版本---echo并发服务器

客户端 发啥 服务器 回啥


#include <stdio.h>
#include <stdlib.h>
#include <errno.h> //errno全局变量
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
//信号的注册函数
void waitpid_func(int signo)
{
while (1)
{
pid_t pid = waitpid(‐1, NULL, WNOHANG);
if (pid > 0)
{
printf("子进程%d退出了\n", pid);
}
else if (pid <= 0)
break;
}
}
void deal_client_fun(int cfd)
{
while (1)
{
unsigned char buf[1500] = "";
int len = recv(cfd, buf, sizeof(buf), 0);
if (len <= 0) //客户端断开连接 会导致 服务器收到0长度报文
break;
send(cfd, buf, len, 0);
}
return;
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("./a.out 8000\n");
return 0;
}
//创建TCP服务器
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0)
{
perror("socket");
_exit(‐1);
}
//绑定固定端口
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (ret < 0)
{
perror("bind");
_exit(‐1);
}
//监听套接字
listen(lfd, 10);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
//给SIGCHLD信号注册 处理函数
signal(SIGCHLD, waitpid_func);//防止子进程出现僵尸进程
//循环提取客户端
while (1)
{
int cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_l
en);
if (cfd < 0)
{
if ((errno == ECONNABORTED) || (errno == EINTR))
continue;
perror("accept");
}
else //提取到正常的客户端
{
char ip_str[16] = "";
unsigned short port = 0;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, 16);
port = ntohs(client_addr.sin_port);
printf("连接:%s %hu到来了\n", ip_str, port);
pid_t pid = fork();
if (pid == 0) //子进程 服务客户端
{
//子进程中lfd无意义
close(lfd);
//子进程 对客户端的 应答程序
deal_client_fun(cfd);
//关闭 已连接套接诶自己cfd
close(cfd);
//退出
_exit(‐1);
}
else //父进程 继续建立提取新的客户端
{
//已连接套接字 无意义
close(cfd);
}
}
}
//关闭监听套接字
close(lfd);
return 0;
}

端口复用概述

默认的情况下,如果一个网络应用程序的一个套接字 绑定了一个端口( 占用了 8000 ),这
时候,别的套接字就无法使用这个端口( 8000 )
端口复用:允许在一个应用程序可以把 n 个套接字绑在一个端口上而不
出错
SO_REUSEADDR可以用在以下四种情况下。 (摘自《Unix网络编程》卷一,即UNPv1)
1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程
序的socket2要占用该地址和端口,你的程序就要用到该选项。
2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个
实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可以测试这种情
况。
3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑
定的ip地址不同。这和2很相似,区别请看UNPv1。
4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多
播,不用于TCP
注意:置端口复用函数要在绑定之前调用,而且只要绑定到同一个端口的
所有套接字都得设置复用

设置套接字端口复用

int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

多线程并发服务器

多线程版本---echo并发服务器


#include <stdio.h>
#include <stdlib.h>
#include <errno.h> //errno全局变量
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <pthread.h>
typedef struct
{
int cfd;
char ip_str[16];
unsigned short port;
} CLI_MSG;
int lfd = 0;
//信号的注册函数
void exit_func(int signo)
{
close(lfd);
_exit(‐1);
}
void *deal_client_fun(void *arg)
{
CLI_MSG tmp = *(CLI_MSG *)arg;
printf("连接:%s %hu到来了\n", tmp.ip_str, tmp.port);
while (1)
{
unsigned char buf[1500] = "";
int len = recv(tmp.cfd, buf, sizeof(buf), 0);
if (len <= 0) //客户端断开连接 会导致 服务器收到0长度报文
{
printf("连接:%s %hu退出了\n", tmp.ip_str, tmp.port);
close(tmp.cfd);
break; //一定要记得<=0
}
send(tmp.cfd, buf, len, 0);
}
pthread_exit(NULL);
return NULL;
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("./a.out 8000\n");
return 0;
}
//创建TCP服务器
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0)
{
perror("socket");
_exit(‐1);
}
//端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//绑定固定端口
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (ret < 0)
{
perror("bind");
_exit(‐1);
}
//监听套接字
listen(lfd, 10);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
//给SIGINT信号注册 处理函数
signal(SIGINT, exit_func); //结束服务器时关闭监听套接字
//循环提取客户端
while (1)
{
int cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_l
en);
if (cfd < 0)
{
if ((errno == ECONNABORTED) || (errno == EINTR))
continue;
perror("accept");
}
else //提取到正常的客户端
{
CLI_MSG tmp;
tmp.cfd = cfd;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, tmp.ip_str,
6);
tmp.port = ntohs(client_addr.sin_port);
//创建线程服务器客户端
pthread_t tid;
pthread_create(&tid, NULL, deal_client_fun, (void *)&tmp);
pthread_detach(tid);
}
}
//关闭监听套接字
close(lfd);
return 0;

web服务器

html(超文本标记语言)显示文本
http(超文本传送协议) 传输协议
URL 统一地址定位符
web服务器使用的传送协议:http(超文本传送协议) 基于TCP
客户端是浏览器 服务器(用户实现)
浏览器只需要提供请求的方式(GET\POST)
每一个客户端 只能有一个请求
浏览器的请求方式:

解析文件名,打开本地文件(成功、失败)

服务器应答的格式:请求失败

HTTP传送文件没有固定大小限制。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h> //errno全局变量
#include <sys/socket.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <signal.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
char err[] = "HTTP/1.1 404 Not Found\r\n"
"Content‐Type: text/html\r\n"
"\r\n"
"<HTML><BODY>File not found</BODY></HTML>";
char head[] = "HTTP/1.1 200 OK\r\n"
"Content‐Type: text/html\r\n"
"\r\n";
typedef struct
{
int cfd;
char ip_str[16];
unsigned short port;
} CLI_MSG;
int lfd = 0;
//信号的注册函数
void exit_func(int signo)
{
close(lfd);
_exit(‐1);
}
//客户端核心任务线程函数
void *deal_client_fun(void *arg)
{
CLI_MSG tmp = *(CLI_MSG *)arg;
printf("连接:%s %hu到来了\n", tmp.ip_str, tmp.port);
//获取浏览器的请求
unsigned char cmd_buf[1024] = "";
int len = recv(tmp.cfd, cmd_buf, sizeof(cmd_buf), 0);
if (len <= 0)
{
close(tmp.cfd);
pthread_exit(NULL);
}
//解析浏览器的请求
char file_name[128] = "./html/";
sscanf(cmd_buf, "GET /%[^ ]", file_name + 7);
if (file_name[7] == '\0')
{
strcat(file_name, "index.html");
}
printf("file_name=##%s##\n", file_name);
//open打开本地文件
int fd = open(file_name, O_RDONLY);
if (fd < 0)
{
//告诉浏览器404
send(tmp.cfd, err, strlen(err), 0);
close(tmp.cfd);
perror("open");
pthread_exit(NULL);
}
//告诉浏览器 200 打开成功准备接受
send(tmp.cfd, head, strlen(head), 0);
//循环读取本地文件数据发送给浏览器
while (1)
{
unsigned char buf[512] = "";
//读取本地文件数据
int len = read(fd, buf, sizeof(buf));
send(tmp.cfd, buf, len, 0);
if (len < 512)
break;
}
close(fd);
close(tmp.cfd);
pthread_exit(NULL);
return NULL;
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("./a.out 8000\n");
return 0;
}
//创建TCP服务器
lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0)
{
perror("socket");
_exit(‐1);
}
//端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//绑定固定端口
struct sockaddr_in my_addr;
bzero(&my_addr, sizeof(my_addr));
my_addr.sin_family = AF_INET;my_addr.sin_port = htons(atoi(argv[1]));
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(lfd, (struct sockaddr *)&my_addr, sizeof(my_addr));
if (ret < 0)
{
perror("bind");
_exit(‐1);
}
//监听套接字
listen(lfd, 10);
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
//给SIGINT信号注册 处理函数
signal(SIGINT, exit_func); //结束服务器时关闭监听套接字
//循环提取客户端
while (1)
{
int cfd = accept(lfd, (struct sockaddr *)&client_addr, &client_l
en);
if (cfd < 0)
{
if ((errno == ECONNABORTED) || (errno == EINTR))
continue;
perror("accept");
}
else //提取到正常的客户端
{
CLI_MSG tmp;
tmp.cfd = cfd;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, tmp.ip_str,
6);
tmp.port = ntohs(client_addr.sin_port);
//创建线程服务器客户端
pthread_t tid;
pthread_create(&tid, NULL, deal_client_fun, (void *)&tmp);
pthread_detach(tid);
}
}
//关闭监听套接字
close(lfd);
return 0;
}