写在前边
本文是B站up主韦东山的4_8-2.TCP编程示例_哔哩哔哩_bilibili视频的笔记,其中有些部分博主也没有理解,希望各位辩证的看。
面向有连接型
面向有连接型种,在发送数据之前,需要在手法主机之间连接一条通信线路。
在面向有连接的方式下,必须在通信传输前后,专门进行建立和断开连接的处理。如果与对端之间无法通信,就可以避免发送无谓的数据。这种传输对应的传输层协议是TCP协议。
面向无连接型
面向无连接型则不要求建立和断开连接。发送端可于任何时候自由发送数据。反之,接收端也永远不知道自己会在何时从哪里 收到数据。因此,在面向无连接的情况下,接收端需要时常确认时候是否收到了数据。
因此,在面向无连接的通信种,不需要确认对端是否催在,即使接收端不存在或无法接收数据,发送端也能将数据发送出去。这种传输对应的传输层协议是UDP协议。
这里对这两个协议只需要大概了解即可,不用管什么三次握手那些,我们主要是使用。
这里不管是什么传输方式,都需要有一个服务器端和用户端进行通信。所以我们要进行编程时,也需要对二者同时进行编程,也就是:server 程序和client 程序。
TCP传输
对于TCP传输,这里借用韦山东老师的图:
图 1 面向连接的 TCP 流模式
这个图中形象的为我们展示了TCP server端和client端的函数,我们只需要按照这个图将其中的函数每个都完善,就可以实现TCP连接了。
接下来是对每个函数的详解:
socket()
函数结构
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
描述
socket() 创建一个通信端点并返回一个引用该端点的文件描述符。 成功调用返回的文件描述符将是当前未为进程打开的最小编号的文件描述符。
参数
- domain
AF_INET这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址。
AF_INET6 与上面类似,不过是来用IPv6的地址。
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用。
还有一些不常用的AF_LOCAL,AF_AX25,AF_IPX等
- type
SOCK_STREAM :是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
SOCK_DGRAM :是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。
SOCK_SEQPACKET:是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
SOCK_RAW:socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)。
SOCK_RDM :这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序。
- protocol
传0 表示使用默认协议。
- 返回值
成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
bind()
函数结构
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
描述
当使用socket创建套接字时,它存在于名称空间(地址族)中,但没有为其分配地址。 bind() 将 addr 指定的地址分配给文件描述符 sockfd 引用的socket。 addrlen 指定addr 指向的地址结构的大小(以字节为单位)。
参数
- sockfd
socket文件描述符,即使用socket()的返回值。
- sockaddr
指向一个struct sockaddr类型的结构体变量,此结构体成员用于设置要绑定的ip和端口
struct sockaddr结构体
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
但是上边这个结构体一般不使用,一般使用下边这个结构体,在使用struct sockaddr_in设置以后要将其强制转化为struct sockaddr类型,然后传值给bind函数。
struct sockaddr_in结构体
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /*设置端口号 */
struct in_addr sin_addr; /*设置ip */
};
- addrlen
第二个参数所指向的结构体变量的大小。
- 返回值
将指定了通信协议的套接字文件与自己的IP和端口绑定起来,成功返回0,失败返回-1
使用示例
int ret;
my_addr.sin_family = AF_INET; //指定协议族为IPV4版本的TCP/IP协议族
my_addr.sin_port = htons(PORT); //指定端口号
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //指定IP地址,这里设置为INADDR_ANY,表示可以接收任何来源的连接请求
ret = bind(isocketfd, (const struct sockaddr*)&my_addr,sizeof(struct sockaddr));
listen()
函数结构
#include<sys/socket.h>
int listen ( int sockfd, int backlog );
描述
监听来自客户端的tcp socket()的连接请求。
参数
- sockfd
socket文件描述符,即使用socket()的返回值。
- backlog
侦听队列的长度。在进程正在处理一个连接请求的时候,可能还存在其它的连接请求。因为TCP连接是一个过程,所以可能存在一种半连接的状态,有时由于同时尝试连接的用户过多,使得服务器进程无法快速地完成连接请求。如果这个情况出现了,服务器进程希望内核如何处理呢?内核会在自己的进程空间里维护一个队列以跟踪这些完成的连接但服务器进程还没有接手处理的连接(还没有调用accept函数的连接),这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限。这个backlog告诉内核使用这个数值作为上限。
- 返回值
成功返回0,失败返回-1
accept()
函数结构
# include <sys/types.h>
# include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
描述
accept()接受一个客户端的连接请求,并返回一个新的套接字(被动监听客户端的三次握手连接请求,三次握手成功即建立连接成功)。所谓“新的”就是说这个套接字与socket()返回的用于监听和接受客户端的连接请求的套接字不是同一个套接字。与本次接受的客户端的通信是通过在这个新的套接字上发送和接收数据来完成的。
参数
- sockfd
用来标识服务端套接字(也就是listen函数中设置为监听状态的套接字)
- sockaddr
具体和bind()的第二个参数操作相同,只是这个是用来保存客户端套接字对应的内存空间变量(包括客户端IP和端口信息等)
- addrlen
参数二内存空间的占地大小。
- 返回值
成功:返回一个服务器用于以后通信的“通信描述符”,失败:返回-1
send()
函数结构
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
描述
TCP协议一般用send函数发送数据。
参数
- sockfd
用于通信的通信描述符,对于服务器,就是指accept函数返回的通信描述符
- buf
指向一片应用缓存,用于存放要发送的数据,存放数据一般使用结构体变量。
- len
存放发送数据的缓存的大小。
- flags
一般设置为0,此时是阻塞发送的,阻塞发送是指发送数据不成功会一直阻塞,直到被某信号中断或发送成功为止,不过发送数据一般不阻塞。
- 返回值
成功:返回发送的字节数,失败:返回-1
recv ()
函数结构
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
描述
不论是客户还是服务器应用程序都用recv函数从TCP连接的另一端接收数据。
参数
- sockfd
指定接收端套接字描述符
- buf
指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据
- len
指明buf的长度。
- flags
传0 表示使用默认协议。
- 返回值
成功:成功执行时,返回接收到的字节数。,失败:返回-1
到这里,所有使用的函数都完成了,现在分别实现server 程序和client 程序。
server
在这个函数中主要实现等待连接然后在有连接出现时,使用fork()函数创建一个子进程,完成和该连接的通信,这样的好处是可以同时实现多个TCP连接。连接成功后等待client发送数据,一旦接收到数据即将数据打印到输出窗口上。
图 2server端窗口显示
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <unistd.h>
/*
* 服务器端程序
* socket
* bind
* listen
* accept
* recv
* close
*/
#define PORT 8888
int main(int argc, char** argv)
{
int isocketfd;
int Client_socketfd;
int ret;
int iRecvLen;
struct sockaddr_in my_addr;
struct sockaddr_in Client_addr;
char buff[1000];
isocketfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == isocketfd)
{
printf("create socket failed!\n");
return -1;
}
my_addr.sin_family = AF_INET; //指定协议族为IPV4版本的TCP/IP协议族
my_addr.sin_port = htons(PORT); //指定端口号
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); //指定IP地址,这里设置为INADDR_ANY,表示可以接收任何来源的连接请求
ret = bind(isocketfd, (const struct sockaddr*)&my_addr,sizeof(struct sockaddr));
if (-1 == ret)
{
printf("bind socket failed!\n");
return -1;
}
//创建监听套接口 监听队列长度为10
ret = listen(isocketfd, 10);
if (-1 == ret)
{
printf("listen socket failed!\n");
return -1;
}
while (1)
{
int Client_socketlen = sizeof(struct sockaddr);
Client_socketfd = accept(isocketfd, (struct sockaddr*)&Client_addr, &Client_socketlen);
if (-1 != Client_socketfd)
{
printf("accept client success!\n");
//生成一个子进程来完成和客户端的会话,父进程继续监听
if (!fork())
{
while (1)
{
//子进程处理客户端的请求
iRecvLen = recv(Client_socketfd, buff, 999, 0);
if (iRecvLen <= 0)
{
close(Client_socketfd);
return -1;
}
else
{
buff[iRecvLen] = '\0';
printf("recv data from client:%s\n", buff);
}
}
close(Client_socketfd);
return 0;
}
}
}
return 0;
}
client
在这个函数中主要实现连接(连接时需要指定server端的IP地址),连接成功后,将在该连接上输出的内容,发送到server端。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
/*
* TCP客户端程序
* socket
* connet
* send
* close
*/
#define PORT 8888
int main(int argc, char** argv)
{
int isocketfd;
struct sockaddr_in client_addr;
char send_buf[1024];
memset(send_buf, 0, sizeof(send_buf));
if (argc < 2)
{
printf("Usage: %s ip_address\n", argv[0]);
return -1;
}
client_addr.sin_family = AF_INET; //指定协议族为IPV4版本的TCP/IP协议族
client_addr.sin_port = htons(PORT); //指定端口号
//指定IP地址,htons()函数是将一个本地字节序的short转为网络字节序的short
client_addr.sin_addr.s_addr = inet_addr(argv[1]);
isocketfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == isocketfd)
{
printf("create socket failed!\n");
return -1;
}
int flag = connect(isocketfd, (struct sockaddr*)&client_addr, sizeof(client_addr));
if (-1 == flag)
{
printf("connect socket failed!\n");
return -1;
}
while (1)
{
printf("please input message to send:\n");
fgets(send_buf, sizeof(send_buf), stdin);
send(isocketfd, send_buf, strlen(send_buf), 0);
}
return 0;
}