TCP-网络编程-实现一个TCP Server

本文介绍了TCP网络编程中的关键步骤,包括使用socket创建连接、bind绑定IP和端口、listen监听请求、connect客户端连接、accept建立连接以及read/write、recv/send等读写操作。同时,详细解析了socket接口的使用,并给出了TCP Server的实现代码。

1. socket

网络层中的IP地址可以唯一确定一台主机,而传输层的 协议+端口 可以唯一表示主机中的应用程序(进程),因此利用三元组(Ip 地址,协议,端口)就可以表示网络中的进程。

进程间的通信是通过socket来完成的,socket类似于文件操作,封装了一组接口,利用这些接口完成**“打开、读/写、关闭操作”**

以TCP协议通信的socket为例,下图是TCP的交互过程:
在这里插入图片描述

具体过程如下:
(1)服务器根据地址类型( ipv4, ipv6 )、 socket 类型、协议创建 socket
(2)bind(): 服务器为 socket 绑定 IP 地址和端口号
(3)listen(): 服务器监听端口号请求,随时接受客户端发来的链接请求,此时,服务器的socket并没有打开
(4)客户端创建socket
(5)connect(): 客户端打开socket,并根据服务器的IP地址和端口号尝试链接服务器socket
(6)accept(): 服务器监听到客户端的socket请求,被动打开socket,开始接收客户端请求,此时,服务端socket进入阻塞状态,即accept()方法,直到客户端返回连接信息(三次握手过程)后才返回。
(7)客户端连接成功,向服务器发送连接状态信息
(8)服务器accept()方法返回,连接成功
(9)send(): 客户端向socket写入信息
(10)recv(): 服务端读取信息
(11)客户端关闭
(12)服务器端关闭

2. 接口详解

(1)socket函数

函数原型: int socket(int domain , int type , int protocol) ;

返回值:整型的socket描述符。 socket函数对应于普通的文件打开操作,普通文件打开操作返回的是一个文件描述符,而socket()函数返回的是socket描述符,它唯一标识一个socket

参数:

参数名描述
domain协议域,又称协议族。

常用地协议族有:AF_INETAF_INET6AF_LOCAL (或称 AF UNIX, Unix 域 socket ) 、 AF_ROUTE 等 。

协议族决定了 socket的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址 ( 32 位) 与端口号( 16 位)的组合 、 AF_ROUTE 决定了要用一个绝对路径名作为地址 。
type指定socket类型。

常用的socket类型:SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET 等 。

SOCK_STREAM: 面向连接的可靠传输,即TCP协议。
SOCK_DGRAM: 面向无连接的不连续不可靠的传输,即UDP协议。
SOCK_RAW : 原始socket,可以读写ICMP及ICMP6、特殊的IP数据包(如自定义包)。
SOCK_PACKET: 与网络驱动程序直接通信,内核将不对网络数据进行处理而直接交给用户。
SOCK_STREAM: 提供连续可靠的数据包连接。
protocol协议

常用协议:IPPROTO_TCP(TCP协议) , IPPTOTO_UDP(UDP协议) , IPPROTO_SCTP(SCTP协议)IPPROTO_TIPC(TIPC协议)

当protocol为0时,自动为type选择对应的协议

socket()函数调用成功,则返回一个新创建的套接字描述符,调用失败,则返回INVALID_SOCKET(linux中为-1)。套接字描述符是一个整型值,每个进程的进程空间里都有一个套接字描述符表,存放着套接字描述符和套接字数据结构的对应关系。

(2)bind函数

函数原型: int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;

通常服务器在启动的时候都会绑定一个众所周知的地址(如 ip 地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的 IP 地址组合 。bind函数就是把一个地址族中的特定地址赋值给socket(包含IP地址和端口号)。

返回值:如果函数执行成功,返回0,否则,返回SOCKET_ERROR

参数:

参数名描述
sockfd即socket描述符。
addr一个 const struct sockaddr*指针,指向要绑定给 sockfd 的协议地址 。

该协议地址结构根据根据创建socket时使用的地址协议族的不同而不同,不同地址协议族的协议地址结构将会在表格下面列出。
addrlen地址的长度

不同地址协议族的地址结构:
IPv4地址结构代码

struct sockaddr_in {
	sa_family_t     sin_family;		// 协议族为:AF_INET
	in_port_t       sin_port;		// 按网络字节顺序的端口号
	struct in_addr  sin_addr;		// 互联网地址
};
// 互联网地址
struct in_addr {
	uint32_t   s_addr;		// 网络字节顺序的地址
};

IPv6地址结构代码

struct sockaddr_in6 {
	sa_family_t     sin6_family;		// 协议族为:AF_INET6
	in_port_t       sin6_port;			// 按网络字节顺序的端口号
	uint32_t		sin6_flowinfo;		// IPv6流信息
	struct in6_addr sin6_addr;			// 互联网地址
	uint32_t		sin6_scope_id;		// 范围ID
};
// IPv6地址
struct in6_addr {
	unsigned char   s6_addr[16];		// IPv6地址
};

UNIX域对应的代码如下:

#define UNIX_PATH_MAX 108
struct sockaddr_un {
	sa_family_t     sun_family;		// 协议族为:AF_UNIX(AF_LOCAL)
	char 			sun_path[UNIX_PATH_MAX];	//路径名
};

(3)listen和connect函数
在服务器端,通过socket()和bind()创建和绑定一个socket之后,会调用listen()函数来监听该socket。在客户端,通过connect()函数发送连接请求,服务端就会收到该请求。

listen和connect函数原型:
int listen(int sockfd , int backlog) ;
int connect(int sockfd , const struct sockaddr *addr, socklen_t addrlen ) ;

listen函数参数说明:

参数名描述
sockfd要监听服务端的socket描述符。
backlog相应 socket 可以排队的最大连接个数

connect函数参数说明:

参数名描述
sockfd客户端的socket描述符。
addr服务器的socket协议地址
addrlen服务器的socket协议地址长度

(4)accept函数
TCP客户端调用connect()函数之后,就会向服务器发送一个连接请求。服务端监听到该请求之后,会调用accept()函数接收请求,建立连接。

函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;

参数:

参数名描述
sockfd服务端的监听socket描述符,即socket()生成后绑定IP并监听的socket描述符。
addr指向 struct sockaddr*的指针,用于返回客户端的协议地址
addrlen客户端的协议地址长度

返回值:如果accept成功,返回值是由内核自动生成的一个全新的socket描述符。

accept函数第一个参数是由socket函数创建的,并绑定了IP地址和端口号,使用listen监听,称为监听socket描述符;而accept函数返回的是已与客户端连接的socket描述符(新创建的)
一个服务器通常仅仅只创建一个监昕 socket 描述字 ,它在该服务器的生命周期 内一直存在 。内核为每个由服务器进程接受的客户创建了一个已连接 socket 描述字,当服务器完成了对某个客户的服务,相应的巳连接 socket 描述字就被关闭 。

(5)读写函数
网络中的IO操作有下面几组:

第一组read() / write():

函数原型:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, void *buf, size_t count);

read从连接成功的socket中读取内容,其中buf为缓冲区,count为缓冲区大小。当读取成功时,返回实际所读取的字节数,如果返回值为0,表示已读取到文件末尾,返回值小于0,表示读取错误。
write从buf缓冲区向连接成功的socket中写入count个数据,返回写入的字节数。如果写入失败,返回-1,并设置errno变量,保存错误信息。


第二组:recv() / send()
函数原型:
int recv(int fd, void *buf, size_t count, int flags);
int send(int fd, void *buf, size_t count, int flags);

recv函数和send函数与read、write函数类似,前三个参数相同,多了一个flags参数。flag可以是0,也可以是以下的组合:

参数名描述
MSG_DONTROUTE**send函数使用的标志。**这个标志表示目的主机在本地网络上,不需要查找表
MSG_OOB表示接收或发送带外的数据 (使用与普通数据不同的通道独立传送给用户,是相连的每一对流套接口间一个逻辑上独立的传输通道)
MSG_PEEK**recv函数的使用标志。**表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容,这样下次读的时候,仍然是一样的内容。一般在有多个进程读写数据时可以使用这个标志。
MSG_WAITALL**recv函数的使用标志。**表示等到所有的信息到达时才返回。使用这个标志的时候recv会一直阻塞,直到指定的条件满足(比如达到读取读取的数量或文件末尾),或者是发生了错误。

第三组:readv() / writev()

第四组:recvmsg() / sendmsg()

第五组:recvfrom() / sendto()


(6)close函数
关闭相应的socket描述符
函数原型:
int close (int fd) ;

注意:close操作只是使相应 socket 描述字的引用计数-1 ,只有当引用计数为 0 的时候,才会触发 TCP 客户端向服务器发送终止连接请求 。

3. TCP server实现代码

server.cpp:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>

#define MAXLINE 4096

int main(int argc, char** argv) {
	int listenfd, connfd;	// socket id of sever and client
	struct sockaddr_in severaddr;	// ip address of server
	
	char buf[MAXLINE];
	int n;
	
	// socket init
	if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
		printf("create sever socket error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	severaddr.sin_port = htons(6666);
	
	// bind socket id
	if (bind(listenfd, (struct sockaddr*)&severaddr, sizeof(severaddr)) == -1) {
		printf("bind sever socket error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	
	// linsten socket id
	if (listen(listenfd, 10) == -1) {
		printf("listen sever socket error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	
	printf("=====Waiting for client's request===\n");
	
	while(1) {
		if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {
			printf("accept sever socket error: %s(errno: %d)\n", strerror(errno), errno);
			return 0;
		}
		
		n = recv(connfd, buf, MAXLINE, 0);
		buf[n] = '\n';
		printf("receive message from clinet: %s\n", buf);
		
		close(connfd);
	}
	close(listenfd);
	return 0;
	
}

client.cpp

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>

#define MAXLINE 4096

int main(int argc, char** argv) {
	int socketfd, n;
	char recvline[MAXLINE], sendline[MAXLINE];
	
	struct sockaddr_in severaddr;	// ip address of server
	
	if (argc != 2) {
		printf("usage: ./client <ipadress>\n");
		return 0;
	}
	
	
	// socket init
	if ((socketfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		printf("create client socket error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	
	memset(&severaddr, 0, sizeof(severaddr));
	severaddr.sin_family = AF_INET;
	severaddr.sin_port = htons(6666);
	
	if (inet_pton(AF_INET, argv[1], &severaddr.sin_addr) < 0) {
		printf("inet_pton error for: %s\n", argv[1]);
		return 0;
	}

	if (connect(socketfd, (struct sockaddr*)&severaddr, sizeof(severaddr)) < 0) {
		printf("connect error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}

	printf("send message to sever: \n");
	fgets(sendline, 4096, stdin);
	
	if (send(socketfd, sendline, strlen(sendline), 0) < 0) {
		printf("send message error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	
	close(socketfd);
	return 0;
	
	
}

makefile:

all:server client
server:server.o
	g++ -g -o server server.o
client:client.o
	g++ -g -o client client.o
server.o:server.cpp
	g++ -g -c server.cpp
client.o:client.cpp
	g++ -g -c client.cpp
clean:all
	rm all

执行方法:
(1)使用make命令编译
(2)在终端输入 ./server ,以此开启服务器(必须要先开启服务器)
(3)新打开一个终端,输入 ./client 127.0.0.1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值