网络编程(socket,TCP协议)

本文详细介绍了TCP编程的基础知识,包括客户端和服务端的通信流程,socket API的使用方法,如socket、bind、listen、accept、connect、read/write、recv/send等函数的讲解及示例代码。通过实例演示了简单的TCP客户端和服务端的通信过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、客户端和服务端的通信图

在这里插入图片描述

2 .socket(TCP)编程API简介

1)socket函数

int socket(int family, int type, int protocol);==

功能:打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据
返回值:如果socket()调用出错则返回-1。
参数:对于IPv4,
family参数指定为AF_INET。
对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定SOCK_DGRAM,表示面向数据报的传输协议。
protocol参数的介绍从略,指定为0即可。

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

功能; 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号, bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。
返回值: bind()成功返回0,失败返回-1。

struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。我们的程序中对myaddr参数是这样初始化的:

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址,端口号为SERV_PORT,我们定义为1234

2)、listen

int listen(int sockfd, int backlog);

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未accept的客户端就处于连接等待状态,
功能: listen() 声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
返回值: listen()成功返回0,失败返回-1。

3)、accept

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。
参数 : cliaddr 是一个传出参数,accept()返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区cliaddr的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给cliaddr参数传NULL,表示不关心客户端的地址。

我们的服务器程序结构是这样的:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);

close(connfd);
}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

4)、connect()

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
返回值: connect()成功返回0,出错返回-1。

5)、Write函数

== Ssize_t write(int fd,const void *buf,size_t nbytes);==

功能: Write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写数据时有两种可能:
返回值: 1.write的返回值大于0,表示写了部分数据或者是全部的数据,这样用一个while循环不断的写入数据,但是循环过程中的buf参数和nbytes参数是我们自己来更新的,也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!
2、返回值小于0,此时出错了,需要根据错误类型进行相应的处理。如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

6)、Read函数

== Ssize_t read(int fd,void *buf,size_t nbyte)==

Read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

7)、Recv函数和send函数

Recv函数和read函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。

Int recv(int fd,void *buf,int len,int flags)
Int send(int fd,void *buf,int len,int flags)

前面的三个参数和read、write函数是一样的。第四个参数可以是0或者是一下组合:

MSG_DONTROUTE:不查找表

是send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。

MSG_OOB:接受或者发生带外数据

表示可以接收和发送带外数据。

MSG_PEEK:查看数据,并不从系统缓冲区移走数据

是recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清楚系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有过个进程读写数据的时候使用这个标志。

MSG_WAITALL:等待所有数据

是recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。

8) 、close的定义如下:

#include<unistd.h>
int close(int fd);

关闭读写。
返回值: 成功则返回0,错误返回-1,错误码errno:EBADF表示fd不是一个有效描述符;EINTR表示close函数被信号中断;EIO表示一个IO错误。

3.示例代码

实现现简单客户端和服务端的通信(基于TCP) 这是实现的是客户端写数据,服务端读取,然后把读取的数据写到客户端显示

1、服务端socket1.c的代码如下

#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<netdb.h>
#include<errno.h>
#define PORT 2345
#define MAXSIZE 1024
int main(int argc, char *argv[])
{
int sockfd, newsockfd;
//定义服务端套接口数据结构
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_zise, portnumber;
//发送数据缓冲区
char buf[MAXSIZE];
//定义客户端套接口数据结构
int addr_len = sizeof(struct sockaddr_in);
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
fprintf(stderr, "create socket failed\n");
exit(EXIT_FAILURE);
}
puts("create socket success");
printf("sockfd is %d\n", sockfd);
//清空表示地址的结构体变量
bzero(&server_addr, sizeof(struct sockaddr_in));
//设置addr的成员变量信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
//设置ip为本机IP
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr*)(&server_addr), sizeof(struct sockaddr)) < 0)
{
fprintf(stderr, "bind failed \n");
exit(EXIT_FAILURE);
}
puts("bind success\n");
if (listen(sockfd, 10) < 0)
{
perror("listen fail\n");
exit(EXIT_FAILURE);
}
puts("listen success\n");
int sin_size = sizeof(struct sockaddr_in);
printf("sin_size is %d\n", sin_size);
if ((newsockfd = accept(sockfd, (struct sockaddr *)(&client_addr), &sin_size)) < 0)
{
perror("accept error");
exit(EXIT_FAILURE);
}
printf("accepted a new connetction\n");
printf("new socket id is %d\n", newsockfd);
printf("Accept clent ip is %s\n", inet_ntoa(client_addr.sin_addr));
printf("Connect successful please input message\n");
char sendbuf[1024];
char mybuf[1024];
while (1)
{
int len = recv(newsockfd, buf, sizeof(buf), 0);
if (strcmp(buf, "exit\n") == 0)
break;
fputs(buf, stdout);
send(newsockfd, buf, len, 0);
memset(sendbuf, 0 ,sizeof(sendbuf));
memset(buf, 0, sizeof(buf));
}
close(newsockfd);
close(sockfd);
puts("exit success");
exit(EXIT_SUCCESS);
return 0;
}

2、客户端socket3.c的代码如下

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<netdb.h>
#include<string.h>
#include<errno.h>
#define PORT 2345
int count = 1;
int main()
{
int sockfd;
char buffer[2014];
struct sockaddr_in server_addr;
struct hostent *host;
int nbytes;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
fprintf(stderr, "Socket Error is %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
//客户端发出请求
if (connect(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr)) == -1)
{
fprintf(stderr, "Connect failed\n");
exit(EXIT_FAILURE);
}
char sendbuf[1024];
char recvbuf[2014];
while (1)
{
fgets(sendbuf, sizeof(sendbuf), stdin);
send(sockfd, sendbuf, strlen(sendbuf), 0);
if (strcmp(sendbuf, "exit\n") == 0)
break;
recv(sockfd, recvbuf, sizeof(recvbuf), 0);
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
exit(EXIT_SUCCESS);
return 0;
}

4、运行结果

在这里插入图片描述

5、总结

服务端:socekt -> bind-> listen->accept

客户端:socket->connect

tcp是面向连接的,安全的,无重复的,排列有序的。

http的通讯过程

1.tcp三次握手
2.发送请求报文
3.服务端进行请求处理,回复响应报文
4.close

获取网页报文

首先打开一个网页,然后按Fn+F12,会弹出一个界面如下
在这里插入图片描述
选择Network,再选择类型(Type)为document的打开,显示如下图,然后选择Headers,即可看到
在这里插入图片描述
在这里插入图片描述
发送网页之前应先发送报文的头部

编写代码,进入网站实现页面登录,成功登录后跳转到二级页面显当前网页信息,登录失败则进入第三界面显示登陆失败

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
typedef struct sockaddr* (SA);

//发送报文
int send_head(char* filename,int conn)
{
	struct stat st;
	int ret = stat(filename,&st);
	if(-1 == ret)
	{
		perror("send head stat error");
		return 1;
	}
	char *cmd[5]={NULL};
	cmd[0]="HTTP/1.1 200 OK\r\n";
	cmd[1]="Connection: keep-alive\r\n";
	cmd[2]="Content-Type: text/html;charset=utf-8\r\n";
	cmd[3]="Server: PTSERVER\r\n";
	//Transfer-Encoding: chunked
	char cont_length[50]={0};
	sprintf(cont_length,"content-length: %lu\r\n\r\n",st.st_size);
	cmd[4] = cont_length;
	int i = 0;
	for(i=0;i<5;i++)
	{
		send(conn,cmd[i],strlen(cmd[i]),0);
	}

}

//发送网页
int send_file(char* filename,int conn)
{
	send_head(filename,conn);
	int fd = open(filename,O_RDONLY);
	if(-1 == fd)
	{
		perror("send_file open error");
		return 1;
	}
	while(1)
	{
		char buf[1024]={0};
		int ret = read(fd,buf,1024);
		if(ret<=0)
		{
			break;
		}
		send(conn,buf,ret,0);
	}
	close(fd);
}
int main(int argc, const char *argv[])
{
	int listfd = socket(AF_INET,SOCK_STREAM,0);
	if(-1 == listfd)
	{
		perror("socket");
		exit(1);
	}

	struct sockaddr_in ser,cli;
	bzero(&ser,sizeof(ser));
	bzero(&cli,sizeof(cli));
	ser.sin_family = AF_INET;
	ser.sin_port = htons(80);
	ser.sin_addr.s_addr = INADDR_ANY;
	int ret = bind(listfd,(SA)&ser,sizeof(ser));
	if(-1 == ret)
	{
		perror("bind error");
		exit(1);
	}
	listen(listfd,3);
	int len = sizeof(cli);
	while(1)
	{
		int conn = accept(listfd,(SA)&cli,&len);//服务器循环响应
		if(-1 == conn)
		{
			perror("accept error");
			exit(1);
		}
		char buf[4096]={0};
		ret = recv(conn,buf,4096,0);
		printf("%s",buf);
		// GET / HTTP/1.1\r\n
		// 报文响应解析,请求主页
		char* method= strtok(buf," ");
		char* url = strtok(NULL," ");
		char* ver= strtok(NULL,"\r");
		if(0 == strcmp(url,"/"))
		{
			send_file("3.html",conn);// login 
		}
		// GET /login?user_name=123&user_pass=123,登录
		if(strstr(url,"login"))//如何判断是请求还是登录
		{
			char *name;
			char* pass;
			char * end;
			name = strstr(url,"user_name");
			name = index(name,'=');//index():在字符串中找字符
			name= name+1;
			end = index(name,'&');
			*end ='\0';
			end=end+1;
			pass = index(end,'=');
			pass= pass+1;
			if(0 ==strcmp(name,"zhangsan") && 0 == strcmp(pass,"123"))
			{
				send_file("1.html",conn);
			}
			else
			{
				send_file("5.html",conn);
			}
		}

		close(conn);
	}
	close(listfd);


	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值