Socket编程实践

之前写过socket编程的原理 , 正好高级网络实验课要写socket程序,故总结一下实践中的一些问题。

Winsock实现

由于我的系统是windows,所以采用Winsock接口。

MFC提供了两个类用以封装Windows Sockets API

  • CAsyncSocket类:具有一定网络编程经验的开发人员
  • CSocket类:由CAsyncSocket类派生,简化网络编程

需要包含Winsock2.h,Winsock32.dll和ws2_32.lib

端口选择:端口1024以前的端口号都是系统保留的或是作为公共服务的,应尽量选择大于1024的端口号

WinSock初始化

Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。

调用任何一个Winsock函数之前都必须检查协议栈安装情况,使用函数WSAStartup() 调用winsock DLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") //加载 ws2_32.dll

void main()
{
WORD wVersionRequested; //指定使用的版本号
WSADATA wsaData; //返回关于Winsock实现的详细信息
wVersionRequested = MAKEWORD(2, 2); // 用宏获得版本号
if (WSAStartup(wVersionRequested, &wsaData) != 0)//初始化ws2_32.dll动态库
{
printf("WSAStartup() failed!\n");//Winsock初始化错误
exit(-1);
}
if (wsaData.wVersion != wVersionRequested)
{
printf("The version of Winsock is not suited!\n");//Winsock版本不匹配
WSACleanup();//结束对ws2_32.dll的调用
exit(-1);
}
//说明ws2_32.dll正确加载
printf("Load ws2_32.dll successfully!\n");
system("pause");
}

创建套接字

当type指定为SOCK_STREAM或SOCK_DGRAM时,因为系统已明确使用tcp和udp来工作,protocol可指定为0

1
2
3
4
5
6
7
8
9
10
11
12
#define SERV_PORT 6789  // 定义端口
#define SERV_IP "127.0.0.1" //定义IP

/* 创建套接字*/
SOCKET ssock = socket(AF_INET, SOCK_STREAM, 0); // 设置IP地址族,socket类型,协议类型,创建一个套接字
//初始化socket addr_in结构
sockaddr_in server;
memset(&server, 0, sizeof(server)); //数据清零
server.sin_family = AF_INET; // IP地址类型
server.sin_addr.s_addr = inet_addr(SERV_IP); //将IP转成网络字节序
server.sin_port = htons(SERV_PORT); //将端口号转为网络字节序
printf("Socket OK!\n");

编译时报错:Use inet_pton() or InetPton() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

问题的解决:
1、用所提示的新函数代替inet_addr函数。
2、修改VS配置,告诉它我就要旧函数,修改方法:项目->属性->C/C++->常规->SDL检查,将“是”改为“否”,即可

主机序与网络字节序

主机序:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序。分为大端和小端两种方式

测试主机序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
//判断本机的字节序
//返回true表为小段序。返回false表示为大段序
bool am_little_endian ()
{
unsigned short i=1;
return (int)*((char *)(&i)) ? true : false;
}
int main()
{
if(am_little_endian()) printf("本机字节序为小段序!\n");
else printf("本机字节序为大段序!\n");
return 0;
}

本机测试结果为:小端序

网络字节序:TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节顺序采用big endian排序方式。

于是,在数据处理时涉及到字节顺序转换的问题。这里用inet_addr htons 处理

绑定端口(服务器)

1
2
3
4
5
6
7
8
9
/*绑定套接字和端口 */
int iSockErr =0;
//建立一个绑定,参数类型需要强转,中间的参数表示指向SOCKADDR结构的地址
iSockErr = bind(ssock, (struct sockaddr*)&server, sizeof(sockaddr));
if (iSockErr == SOCKET_ERROR) {
WSAGetLastError();//根据不同的错误类型进行不同的处理
exit(1);
}
printf("Bind OK!\n");

监听端口(服务器)

1
2
3
4
5
6
7
/* 监听连接请求 */
iSockErr = listen(ssock, 128); // 限定同时建立连接的客户端数量
if (iSockErr == SOCKET_ERROR) {
WSAGetLastError();//根据不同的错误类型进行不同的处理
exit(1);
}
printf("Listenning......\n");

发起连接(客户端)

在客户端使用connect请求建立连接时,将激活建立连接的三次握手,用来建立一条到服务器TCP的连接。

1
2
3
4
5
6
7
/* 发起连接请求*/
int iSockErr = 0;
iSockErr = connect(sock, (struct sockaddr*)&server, sizeof(sockaddr));
if (iSockErr == SOCKET_ERROR) {
WSAGetLastError();//根据不同的错误类型进行不同的处理
exit(1);
}

处理连接请求(服务器)

accept用于面向连接的服务器端,在IP协议族中,只用于TCP服务器端

accept接受一个socket的连接请求,同时返回一个新的socket,新的socket用来在服务器与客户端之间传递和接收信息

此时socket表示处于监听状态的socket,地址是客户机的IP地址

1
2
3
4
5
/*阻塞等待客户端发起连接,三次握手成功建立连接后会返回一个新的文件描述符指向客户端socket,用于真正数据传输*/
SOCKADDR client;
int nSize = sizeof(SOCKADDR);
SOCKET csock = accept(ssock, (SOCKADDR*)&client, &nSize);
printf("Connect OK!\n");

获取客户端的数据并处理(服务器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 客户端
while (1)
{
char buffer[1024] = "\0";
printf("Input the string you want transfer-------------\n");
scanf("%s", buffer);
if (send(sock, buffer, sizeof buffer, 0) != SOCKET_ERROR)
{
if (recv(sock, buffer, sizeof buffer, 0) != SOCKET_ERROR)
printf("Received datagram from TCP server:%s\n", buffer);
}
}

// 服务器
while (1)
{
char buffer[1024] = "\0";
printf("Waiting for message from client-------------\n");
if (recv(csock, buffer, sizeof buffer, 0) != SOCKET_ERROR) //从recv缓冲区读入数据,没有数据会阻塞
{
for (int i = 0; i < sizeof buffer; i++) {
char ch = buffer[i];
if (ch >= 'a'&&ch <= 'z')
buffer[i] = ch - 32;
}
////给cilent发转为大写的数据
send(csock, buffer, sizeof buffer, 0);

}
Sleep(500);
}

关闭套接字

1
2
3
4
5
6
//关闭套接字
closesocket(ssock);
closesocket(csock);

//终止 DLL 的使用
WSACleanup();

Linux实现

Windows 下的 socket 程序和 Linux 思路相同,但细节有所差别:

1) Windows 下的 socket 程序依赖 Winsock.dll 或 ws2_32.dll,必须提前加载。

2) Linux 使用“文件描述符”的概念,而 Windows 使用“文件句柄”的概念;Linux 不区分 socket 文件和普通文件,而 Windows 区分;Linux 下 socket() 函数的返回值为 int 类型,而 Windows 下为 SOCKET 类型,也就是句柄。

3) Linux 下使用 read() / write() 函数读写,而 Windows 下使用 recv() / send() 函数发送和接收。

4) 关闭 socket 时,Linux 使用 close() 函数,而 Windows 使用 closesocket() 函数。

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <arpa/inet.h> // 包含 sockaddr_in 结构体
#include <ctype.h> // 大小写转换
#include <string.h> // 包含bzero
#define SERV_PORT 6666
#define SERV_IP "127.0.0.1"

int main() {
int lfd,cfd;
struct sockaddr_in serv_addr,client_addr;
socklen_t client_addr_len,client_IP_len;
char buf[BUFSIZ],client_IP[BUFSIZ];
int n,ret;

lfd = socket(AF_INET, SOCK_STREAM, 0); //创建一个套接字,返回文件描述符指向服务器socket
if (lfd == -1) {
perror("socket error"); // 如果在连接状态先关掉server,那么并不会真正关闭,端口还占用,下一次启动就会出现问题
exit(1);
}

//结构体初始化
bzero(&serv_addr, sizeof(serv_addr)); // 缓冲区清零
serv_addr.sin_family = AF_INET; // IP地址类型
serv_addr.sin_port = htons(SERV_PORT); //将端口号转为网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //将IP转成网络字节序,INADDR_ANY自动获取当前网卡上有效IP

ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); //建立一个绑定,参数类型需要强转
if (ret == -1) {
perror("bind error");
exit(1);
}

ret = listen(lfd, 128); // 限定同时建立连接的客户端数量
if (ret == -1) {
perror("listen error");
exit(1);
}

client_addr_len = sizeof(client_addr);
//阻塞等待客户端发起连接,三次握手成功建立连接后会返回一个新的文件描述符指向客户端socket,用于真正数据传输
cfd = accept(lfd, (struct sockaddr*)&client_addr, &client_addr_len);
if (cfd == -1) {
perror("accept error"); // 如果在连接状态先关掉server,那么并不会真正关闭,端口还占用,下一次启动就会出现问题
exit(1);
}

printf("client IP:%s,client port:%d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr,client_IP,sizeof(client_IP),
ntohs(client_addr.sin_port));

while (1) {
n = read(cfd, buf, sizeof(buf)); //从read缓冲区读入数据,没有数据会阻塞
for (int i = 0; i < n; i++) {
buf[i] = toupper(buf[i]);
}
write(cfd, buf, n); // socket双向通信,故有两个缓冲区,双向全双工
// read 和 write 缓冲区在内核中, 而char buf[] 是用户定义的,在stack中
}

close(lfd);
close(cfd);

return 0;

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h> // memset 的头文件
#include <arpa/inet.h> // 包含 sockaddr_in 结构体

#define SERV_PORT 6666
#define SERV_IP "127.0.0.1"

int main() {
int cfd;
struct sockaddr_in serv_addr;
// socklen_t serv_addr_len;
char buf[BUFSIZ];
int n;

cfd = socket(AF_INET, SOCK_STREAM, 0);

//结构体初始化
memset(&serv_addr,0,sizeof(serv_addr); // 指针清空,防止默认取随机值
serv_addr.sin_family = AF_INET; // IP地址类型
serv_addr.sin_port = htons(SERV_PORT); //将端口号转为网络字节序
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);

connect(cfd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); // linux 会隐式bind

while (1)
{
fgets(buf, sizeof(buf), stdin);//输入要处理的数据,没有数据会阻塞
write(cfd, buf, strlen(buf); // 将从键盘接收的数据写入write
n = read(cfd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}

close(cfd);

return 0;
}