基于LINUX的TCP通信

前言

tcp是面向连接的,安全的,无重复的,排列有序的。
服务端初始化流程:socekt() ----> bind()----> listen()---->accept()
客户端初始化流程:socket()---->connect()

我们看到使用了很多接口,之所以在写之前,我们 必须了解各种网络编程API,各个参数的作用,使用的顺序与限制

网络编程接口API

1.socket

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

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

2.bind

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

bind()的作用是将参数sockfd和myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符
监听myaddr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,myaddr参 数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数,addrlen指定结构体的长度。

struct sockaddr_in server_addr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(sockfd, (struct sockaddr *)(&server_addr), sizeof(struct sockaddr))

先讲一下struct sockaddr_in server_addr这个结构体变量因为初始化完后要需要绑定到套接字上,在bind函数中,却是struct sockaddr *参数指针与前面有差异,下面我们就介绍一下这两个结构体的差异:
1)sockaddr
sockaddr在/usr/include/bits/socket.h下,查看sockaddr的结构:

struct sockaddr {
    
    
	sa_family_t sa_family;
	char sa_data[14];
};

sockaddr的缺陷:sa_data把目标地址和端口信息混在一起了。而sockaddr_in就解决了这一缺陷,将端口号和IP地址分开存储。
2)sockaddr_in
sockaddr_in在/usr/include/netinet/in.h下,查看sockaddr_in的结构:

struct sockaddr_in {
    
    
	sa_family_t sin_family;
	in_port_t sin_port;
	struct in_addr sin_addr;
	uint8_t sin_zero[8];//用于填充的0字节
};

3)sockaddr_in与sockaddr两者的区别与联系:
在这里插入图片描述
联系:二者的占用的内存大小是一致的,因此可以互相转化,从这个意义上说,他们并无区别。
区别:sockaddr常用于bind、connect、recvfrom、sendto等函数的参数,指明地址信息,是一种通用的套接字地址。
而sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作。使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。
4)网络字节序与主机字节序
在初始化中我们用到了两个接口htonl()与htons()顺便再多解释两个:
1.主机字节序
就是我们平常说的大端和小端模式,大端就是低地址存放高字节,小端就是低地址存放低字节。不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。
2.网络字节序
内存地址有大小端之分,网络数据流同样有大端小端之分。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
举个例子:4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这就是大端字节序。也就是TCP/IP首部中所有的二进制整数在网络中传输时都要求的这种次序。

主机字节序与网络字节序的转换函数:

#include <arpa/inet.h>
/*将32位的长整数从主机字节序转换为网络字节序,*/ 
uint32_t htonl(uint32_t hostlong);
 /*将16位的短整数从主机字节序转换为网络字节序,*/
  uint16_t htons(uint16_t hostshort); 
 /*将32位的长整数从网络字节序转换为主机字节序,*/
  uint32_t ntohl(uint32_t netlong); 
 /*将16位的短整数从网络字节序转换为主机字节序,*/ 
 uint16_t ntohs(uint16_t netshort);

这样记忆,h代表host(本地主机),n代表net(网络),l是unsigned long(无符号长整型)。
如果是小端字节序,这些函数就会将参数转换为大端返回,如果是大端字节序,不做转换,直接返回。
我们一般为了简化编程,将IP地址设置为INADDR_ANY,如果需要使用特定的IP地址则需要使用inet_addr 和inet_ntoa完成字符串和in_addr结构体的互换,in_addr是SOCKADDR_IN成员,其代表IP地址。
当遇到是字符串类的我们可以使用:

 //将字符串转换为in_addr类型  
 sock.sin_addr.S_un.S_addr =  inet_addr("192.168.1.111");  
 sock.sin_port = htons(5000);  
 //将in_addr类型转换为字符串  
 printf("inet_ntoa ip = %s\n",inet_ntoa(sock.sin_addr));

这时候我们初始化的情况就弄明白了

3.listen

int listen(int sockfd, int backlog);

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

4.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(),内核会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。
5.connect

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

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数是自己的地址,而connect的参数是对方的地址。
6.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表示网络连接出现了问题。

7.read

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

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

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

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

服务器端socket编程

#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);//server_addr.sin_addr.s_addr = inet_addr("192.168.69.129"),指定ip创建服务器端
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;
}

客户端socket编程

#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);//server_addr.sin_addr.s_addr = inet_addr("192.168.69.129"),指定ip的代码
//客户端发出请求
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;
}

直接可编译进行连接,为了简化编程,我们一般将ip地址直接用INADDR_ANY代替,如果想要输入指定ip字符串,可以使用inet_addr()接口转换,这几个ip的转换前面已经区分了

结果(指定了ip为192.168.69.129的服务端,因为是在一个虚拟机上,所以客户端ip=服务器ip)

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42271802/article/details/108713114