不会吧,到了2021年了还有人学习c语言?还有人用最底层的函数实现?这种巴拉巴拉的话是我写这篇东西的动力所在,大家都清楚,底层c用的是顺序逻辑式的、相比面向对象是逻辑更复杂的一种思维,所以现在很多追求简单快捷的公司人员很是唾弃这种思维,因为很多前辈已经封装处理好了这样的对象,只需调用即可,相对c这样的方式,面向对象的优雅和简便就这样体现的,但我就喜欢深挖。
前言
这里要说明记录的是关于TCP和UDP的socket编程,是基于c语言的编程开发,涉及linux系统和windows系统,所以会有两种形态。首先是面向连接的tcp协议,这种协议强调连接,数据信息稳定发送,这种情况需要在服务端创建两个socket,一个用来监听连接请求的,另外一个负责发送和接收数据的;而客户端则只需要一个socket负责发起连接请求和发送/接收数据即可。
一、简单实现
这上面就是关于TCP连接的三次握手中的多数成功连接的流程:客户端发起连接请求,发送SYN报文,产生随机值J,客户端进入SYN_SENT状态;对应的服务端收到报文以后,回复确认报文同样数值的SYN和ACK,并对应J回复+1后的ack,这边也产生一个随机值K,都发送过去后,服务端进入SYN_RECV状态;客户端检查ack报文和ACK,如果正确就回复ACK=1,ack = K+1,待服务端检查报文就进入ESTABLISHED状态。
这也是最常见的一种情况,作为提供服务的服务端往往处于长期的工作,并不发起连接,只监听客户端的连接请求,并且服务完成,一般也是客户端主动断开连接。
以下是一个简单代码,tcp连接发送"Hello, world"。
//hello_server.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
void errorHandling(char *message);
int main(int argc, char *argv[]) {
int serv_sock;
int clnt_sock;
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;
char message[] = "Hello, world!";
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
//创建socket套接字
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == -1)
errorHandling("socket() error!");
//构建服务端地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//告诉内核将serv_sock套接字和serv_addr套接字地址联系起来
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("bind() error!");
//把serv_sock套接字转换成监听套接字,接收客户端连接请求
if (listen(serv_sock, 5) == -1)
errorHandling("listen() error!");
//在serv_sock套接字监听到客户端连接请求后,保存客户端地址信息到clnt_addr
//并且返回已连接套接字描述符给clnt_sock
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_addr, &clnt_addr_size);
if (clnt_sock == -1)
errorHandling("accept() error!");
//发送信息
write(clnt_sock, message, sizeof(message));
//断开连接
close(clnt_sock);
close(serv_sock);
return 0;
}
void errorHandling(char *message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
//hello_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int main(int argc, char *argv[]) {
int sock;
struct sockaddr_in serv_addr;
char message[30];
int str_len;
if (argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(-1);
}
//创建socket套接字
sock = socket(PF_INET, SOCK_STREAM, 0);
if (sock == -1) {
printf("socket() error!");
exit(-1);
}
//构建服务端地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//发起连接请求,一直阻塞到连接成功建立或者发生错误
if(connect(sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1) {
printf("connect() error!");
exit(-1);
}
//读取服务端发送的信息
str_len = read(sock, message, sizeof(message) - 1);
if (str_len == -1) {
printf("read() error!");
exit(-1);
}
printf("server: %s", message);
//断开连接
close(sock);
return 0;
}
上面是客户端和服务端的对应代码,那我们来正常看一下运行结果:
#server
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ gcc hello_server.c -o hello_server
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./hello_server 9999
#client
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ gcc hello_client.c -o hello_client
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./hello_client 172.26.182.175 9999
server: Hello, world!
结果很明显,服务端对于客户端发起的请求进行了连接,然后回送了"Hello, world"信息,这是在本机的WSL2的ubuntu子系统进行的,如果用127.0.0.1进行回流也能成功。
总结一下,代码走的流程如下:
需要提醒的是,connect发起和accept接受是不可测的,就是不一定规定是谁先谁后的,有可能客户端发起了connect请求在前,有可能在服务端发起accept以后,而且客户端发起connect是给了一个请求,然后进入了服务端的连接请求队列,而服务端在调用了accept以后,就会处于一个阻塞状态,就是一直等,等客户端的connect。
二、详细讲讲
首先,网络应用往往是基于客户端-服务端的模型,而客户端和服务端指的都是进程,而不是常说的主机,因为一台主机可以同时运行多个服务进程也可以运行多个客户端进程,对于主机,网络是一种特别的IO设备。
第二,网络编程常常是应用套接字接口和IO接口进行,一个套接字就是通信的一个端点,从linux一切都是文件的角度来看,socket套接字就是有响应描述符的打开文件,在windows中就是句柄,不过windows区分文件句柄和套接字句柄。
第三,服务端和客户端常常使用socket函数来创建socket套接字,而函数的作用是返回部分打开的socket套接字,尚不能用来读写,并且内核会默认socket函数创建的是主动套接字,而这往往存在于连接的客户端中,服务器作为等待连接的被动实体是不持有主动套接字的,所以上面我们使用listen函数把socket得到的主动套接字转换成了监听套接字。
第四,socket返回套接字在客户端中需要connect()成功才能进行读写,而在服务器中则是accept()返回的已连接套接字来与客户端进行通信,也就是说,服务端存在一个监听的socket套接字和一个只存在于与客户端连接成功到断开连接的已连接socket套接字。
然后记录一下一些关于socket种类的信息:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//domain,协议族信息
//type,套接字数据传输类型信息
//protocol,计算机通信使用的协议
这里的协议族信息和套接字类型以及计算机通信协议都是int类型,但它已经使用枚举类型来界定好可选信息,比如:
名称 | 协议族 |
---|---|
PF_INET | IPv4互联网协议族 |
PF_INET6 | IPv6互联网协议族 |
PF_LOCAL | 本地通信的UNIX协议族 |
PF_PACKET | 底层套接字的协议族 |
PF_IPX | IPX Novell协议族 |
表1—1,sys/socket.h中声明的协议族信息
需要了解的套接字类型基本就两种:SOCK_STREAM和SOCK_DGRAM,前者代表面向连接的TCP套接字,后者代表面向信息的UDP套接字。对于socket函数的第三个参数,是对于前面两个参数的规范和确定吧,因为有时候会存在同一协议族存在多个数据传输方式相同的协议,不过这里用的最多就一个0就行了。
对于tcp协议会有linux和windows的两种实现是吧,现在就补充一下。重申一个重要概念,linux中一切皆文件,所以socket函数在linux中返回的叫做文件描述符,这个是个通用概念。微软为了方便拓展,把socket函数的返回值类型定为SOCKET类型,并且使用SOCKET结构体来保存套接字句柄,同时发生错误的返回值也是一些枚举方便区分,比如INVALID_SOCKET。
以下是上面helloworld例子的windows实现:
//hello_server.cpp
//定义_WINSOCK_DEPRECATED_NO_WARNINGS使得inet_addr在这里的使用不报警
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <winsock2.h>
using namespace std;
//链接外部库
#pragma comment(lib, "ws2_32.lib")
void errorHandling(const char *message);
int main(int argc, char *argv[]) {
WSADATA wsa_data;
SOCKET serv_sock, clnt_sock;
SOCKADDR_IN serv_addr, clnt_addr;
char message[] = "Hello, world!";
int clnt_addr_size;
if (argc != 2) {
cout << "Usage: " << argv[0] << " <port>" << endl;
exit(-1);
}
//winsock编程需要的初始化,设置程序所需winsock版本
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if (serv_sock == INVALID_SOCKET)
errorHandling("socket() error!");
//设置serv_addr地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//绑定serv_sock和serv_addr
if (bind(serv_sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("bind() error!");
//serv_sock对连接请求进行监听
if (listen(serv_sock, 5) == SOCKET_ERROR)
errorHandling("listen() error!");
//接受连接队列里面的客户端请求
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (SOCKADDR*)&clnt_addr, &clnt_addr_size);
if (clnt_sock == INVALID_SOCKET)
errorHandling("accept() error!");
//发送信息
send(clnt_sock, message, sizeof(message), 0);
//关闭socket套接字和注销库,归还给windows系统
closesocket(clnt_sock);
closesocket(serv_sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(const char *message) {
cout << message << endl;
exit(-1);
}
//hello_client.cpp
//定义_WINSOCK_DEPRECATED_NO_WARNINGS使得inet_addr在这里的使用不报警
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <winsock2.h>
using namespace std;
//链接外部库
#pragma comment(lib, "ws2_32.lib")
void errorHandling(const char* message);
int main(int argc, char *argv[]) {
WSADATA wsa_data;
SOCKET h_socket;
SOCKADDR_IN serv_addr;
char message[30];
int str_len;
if (argc != 3) {
cout << "Usage: " << argv[0] << " <IP> <port>" << endl;
exit(-1);
}
//winsock编程需要的初始化,设置程序所需winsock版本
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
h_socket = socket(PF_INET, SOCK_STREAM, 0);
if (h_socket == INVALID_SOCKET)
errorHandling("socket() error!");
//设置serv_addr地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//发起连接请求
if (connect(h_socket, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("connect() error!");
//读取服务端发送的信息并输出
str_len = recv(h_socket, message, sizeof(message) - 1, 0);
if (str_len == -1)
errorHandling("read() error!");
cout << message << endl;
//关闭socket套接字和注销库,归还给windows系统
closesocket(h_socket);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(const char* message) {
cout << message << endl;
exit(-1);
}
这里是在vs2017下进行的c++实现,但使用的都是C函数库。
#server
PS C:\Users\samu\Desktop> ./hello_server 9999
请按任意键继续. . .
PS C:\Users\samu\Desktop>
#client
PS C:\Users\samu\Desktop> ./hello_client 127.0.0.1 9999
Hello, world!
请按任意键继续. . .
PS C:\Users\samu\Desktop>
这个是windows的程序互联,你也可以使用linux下进行互联。关于WSAStartup函数这里只简单看用法,先不计较它的具体参数。
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData);
上面介绍了关于socket套接字的知识,还有个重要的东西,就是目标地址,我们访问url往往包含了协议以外,还有关于ip地址、端口号和其他各种信息,这些信息同样重要!这方面信息主要由结构体来进行包含。
//bind函数适用的地址信息
struct sockaddr_in {
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
};
//上面提及的in_addr结构体
struct in_addr {
in_addr_t s_addr; //32位IP地址
};
//bind函数的期望参数sockaddr结构体
struct sockaddr {
sa_family_t sin_family; //地址族信息
char sa_data[14]; //地址信息,IP和端口号
};
上面就是经常使用到的地址信息结构体,虽然在windows中的命名不太一样但基本信息不变动,在我们编写代码时,往往需要对服务端的地址信息进行输入绑定,并在bind函数或者connect函数中使用,因为服务端的IP和端口号或者域名信息往往是定好的,不怎么需要变化,只有客户端的IP才是基本不定的,当然我们也可以灵活编程实现。具体怎么实现?sin_addr给定死就是不变,给的inet_addr(argv[2])就是根据输入而定。
一个重要概念の字节序和网络字节序
对于不同CPU,数据在内存空间中的保存方式是不一样的。比如对于1,用二进制表示可以是:
00000000 00000000 00000000 00000001
也可以是:
00000001 00000000 00000000 00000000。
对于这两种保存方式,前者叫大端序的数据保存,即高位字节存放低位地址,就是我们看的最自然的方式;另一种叫小端序,即高位字节存放高位地址,就是上面的第二种方式。尹圣雨所著的书里面有个更明了的例子,比如4字节int型数0x12345678,然后大端序的保存方式如下:
0x12 0x34 0x56 0x78
小端序则是如下:
0x78 0x56 0x34 0x12
然后主流的intel系列CPU采用小端序保存数据,而为了统一,网络字节序为大端序,我们前面用到的htons和htonl函数就是负责进行网络转换的函数,它的命名也是极为简约:h,主机序;n,网络序;s和l则是代表数据类型,s为short,l为long,htons就是short数据从主机序转换成网络序。与之类似的函数还有:htonl函数、ntohs函数、ntohl函数,具体用法参照上面htons。
另一个重要概念の字符串信息和网络字节序的转换
学习过网络的同学应该知道,IP地址的表示对于我们常人容易理解的就是点分十进制表示法,比如127.0.0.1就表示本机地址,而sockaddr_in保存的IP地址为32位整型数,即0101这种数值,所以需要有一个转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char *str); //得到32位大端序整型数,失败返回INADDR_NONE
int inet_aton(const char *str, struct in_addr *addr); //利用了in_addr结构体,和inet_addr有相同功能,使用频率更高
char* inet_ntoa(struct in_addr addr); //转换整数型IP为字符串格式并返回,失败时返回-1
以上是不依赖于特定平台,应用广泛的转换函数,对于专属型的函数,也有一些,比如windows平台的WSAStringToAddress和WSAAddressToString,这两个函数功能上和inet_addr和inet_ntoa相同,但支持多种协议,IPv4和IPv6中都适用,但过于依赖平台。
三、关于UDP
相对TCP有来有回的连接,UDP就不一样,它是一去不回的,定了地址信息就发出去然后没有什么回响的。所以TCP比UDP更为可靠,但由于TCP会对信息的发送回应ACK应答,并给数据包分配序号,所以UDP就显得更为简洁。
前面的TCP连接,需要至少两个套接字来进行服务,一个负责看门监听连接请求,一个负责连接收发信息,但UDP只需要一个socket套接字即可。另外由于不同于TCP,它的收发信息函数也需包含地址信息。
#include <sys/socket.h>
ssize_t sendto(int sock, void* buff, size_t nbytes, int flags,
struct sockaddr* to_addr, socklen_t addr_len);
ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags,
struct sockaddr* from_addr, socklen_t addr_len);
上面基本需要注意的就是sockaddr结构体类型指针参数了,这个参数指明了目标地址,尤为重要。
//uecho_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF 30
void errorHandling(char* message);
int main(int argc, char* argv[]) {
int serv_sock, str_len;
socklen_t clnt_addr_size;
struct sockaddr_in serv_addr, clnt_addr;
char message[BUF];
if(argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(-1);
}
//创建套接字
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
errorHandling("socket() error!");
//构造地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//绑定套接字
if (bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
errorHandling("bind() error!");
while(1) {
clnt_addr_size = sizeof(clnt_addr);
str_len = recvfrom(serv_sock, message, BUF, 0,
(struct sockaddr*) &clnt_addr, &clnt_addr_size);
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*) &clnt_addr, clnt_addr_size);
}
//关闭销毁套接字资源
close(serv_sock);
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
//uecho_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF 30
void errorHandling(char* message);
int main(int argc, char* argv[]) {
int sock, str_len;
char message[BUF];
socklen_t addr_size;
struct sockaddr_in serv_addr, dest_addr;
if(argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(-1);
}
//创建套接字
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
errorHandling("socket() error!");
//构建地址信息
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//信息回流
while(1) {
printf("me(q to quit): ");
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&serv_addr, sizeof(serv_addr));
addr_size = sizeof(dest_addr);
str_len = recvfrom(sock, message, BUF, 0,
(struct sockaddr*) &dest_addr, &addr_size);
message[str_len] = 0;
printf("back: %s", message);
}
close(sock);
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
这是基于UDP的一个回声器,你说啥它说啥那种,然后使用如下:
#server
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./uecho_server 9999
^C#强行停止
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$
#client
jack@DESKTOP-SJO8SMG:/mnt/c/Users/samu$ ./uecho_client 172.26.179.14 9999
me(q to quit): mieamieamiea
back: mieamieamiea
me(q to quit): 咩啊咩啊
back: 咩啊咩啊
me(q to quit): balabala
back: balabala
me(q to quit): q
这下面就是windows中的实现了,所使用的收发函数是一样的,虽然类型有变动,但实质没变。
//uecho_server.c
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF 30
#pragma comment(lib, "ws2_32.lib")
void errorHandling(char* message);
int main(int argc, char* argv[]){
WSADATA wsa_data;
SOCKET serv_sock;
SOCKADDR_IN serv_addr, from_addr;
char message[BUF];
int str_len, addr_size;
if (argc != 2){
printf("Usage: %s <port>\n", argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == INVALID_SOCKET)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (bind(serv_sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR)
errorHandling("bind() error!");
while (1) {
addr_size = sizeof(from_addr);
str_len = recvfrom(serv_sock, message, BUF, 0,
(SOCKADDR*)&from_addr, &addr_size);
sendto(serv_sock, message, str_len, 0,
(SOCKADDR*)&from_addr, sizeof(from_addr));
}
closesocket(serv_sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
//uecho_client.c
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF 30
#pragma comment(lib, "ws2_32.lib")
void errorHandling(char* message);
int main(int argc, char* argv[]){
WSADATA wsa_data;
SOCKET sock;
SOCKADDR_IN serv_addr, dest_addr;
char message[BUF];
int str_len, addr_size;
if (argc != 3){
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
while (1) {
fputs("me(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
addr_size = sizeof(dest_addr);
sendto(sock, message, strlen(message), 0,
(SOCKADDR*)&serv_addr, sizeof(serv_addr));
str_len = recvfrom(sock, message, BUF, 0,
(SOCKADDR*)&dest_addr, &addr_size);
message[str_len] = 0;
printf("back: %s", message);
}
closesocket(sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
实验结果如下:
#server
PS C:\Users\samu\Desktop> ./uecho_server 9999
PS C:\Users\samu\Desktop>
#client
PS H:\project\c&c++ course\socketpro\Release> ./socketpro 192.168.2.2 9999
me(q to quit): 咩啊咩啊咩啊
back: 咩啊咩啊咩啊
me(q to quit): balabala
back: balabala
me(q to quit): q
请按任意键继续. . .
对于TCP,它的数据传输不存在边界,就是调用I/O函数的次数不重要,信息的发送接收和缓存有关;相反的,UDP的信息发送存在边界,调用I/O函数的次数非常重要,输入和输出的调用次数应该一致。UDP通过sendto函数发送数据每次都会重复:UDP套接字注册目标IP和端口,传输数据,删除UDP套接字中注册信息。因此如果和同一台主机一直这样传输信息,效率会很低下,所以就效率考虑,可以创建已连接UDP套接字,针对同一主机的通信,然后上面的uecho客户端可以更改为这样:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#define BUF 30
#pragma comment(lib, "ws2_32.lib")
void errorHandling(char* message);
int main(int argc, char* argv[]) {
WSADATA wsa_data;
SOCKET sock;
SOCKADDR_IN serv_addr, dest_addr;
char message[BUF];
int str_len, addr_size;
if (argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(-1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsa_data) != 0)
errorHandling("WSAStartup() error!");
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == INVALID_SOCKET)
errorHandling("socket() error!");
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
//addition
connect(sock, (SOCKADDR*)&serv_addr, sizeof(serv_addr));
while (1) {
fputs("me(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
send(sock, message, strlen(message), 0);
str_len = recv(sock, message, sizeof(message) - 1, 0);
message[str_len] = 0;
printf("back: %s", message);
}
closesocket(sock);
WSACleanup();
system("pause");
return 0;
}
void errorHandling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(-1);
}
可以看到上述代码添加了connect函数来对UDP套接字进行连接,而且由于是已连接UDP,可以使用send和recv函数来替换需要目标地址的sendto和recvfrom函数来进行通信。
四、TCP的连接断开
前面有TCP的三次握手建立连接,对于一个可靠的连接,断开当然也会有不少功夫,事实上,TCP的断开被称为四次分手(别人喜欢这么叫)
简单流程如上,当客户端调用close主动关闭连接,客户端发送一个FIN分节,随机产生一个M报文给服务端,表示数据发送over,客户端进入FIN_WAIT_1状态;接收到FIN的服务端执行被动关闭,接收到的其实就是文件结束符(end-of-file),回送确认报文M+1,服务端进入CLOSE_WAIT状态,收到确认报文的客户端进入FIN_WAIT_2状态;隔上一段时间,服务端调用close关闭套接字,发送一个FIN给客户端,随机产生一个N报文,进入LAST_ACK状态;客户端接收到FIN报文和随机N,回复确认报文N + 1进入TIME_WAIT状态,服务端接收到确认报文后进入CLOSED状态,客户端经过一段时间等待也就进入CLOSED状态。
但对于我们来说,就只看到了close调用了而已,这个是作为补充知识,而且断开连接流程并不严格按照上面分得条条道道那样进行,有时候FIN报文会跟着最后发送的信息一起发送,也就是第一次分手融进消息发送里,有时候第二步和第三步连在一起发送,就是服务端收到FIN报文就回复确认报文和调用close发送FIN了。