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_INET、 AF_INET6 、 AF_LOCAL (或称 AF UNIX, Unix 域 socket ) 、 AF_ROUTE 等 。 协议族决定了 socket的地址类型,在通信中必须采用对应的地址,如 AF_INET 决定了要用 ipv4 地址 ( 32 位) 与端口号( 16 位)的组合 、 AF_ROUTE 决定了要用一个绝对路径名作为地址 。 |
| type | 指定socket类型。 常用的socket类型:SOCK_STREAM 、 SOCK_DGRAM 、SOCK_RAW 、 SOCK_PACKET 、 SOCK_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
本文介绍了TCP网络编程中的关键步骤,包括使用socket创建连接、bind绑定IP和端口、listen监听请求、connect客户端连接、accept建立连接以及read/write、recv/send等读写操作。同时,详细解析了socket接口的使用,并给出了TCP Server的实现代码。
1167

被折叠的 条评论
为什么被折叠?



