哎呀,其实这次是 network lab 了
UDP 连接
这一次是一个 serv 和一个 client
这一个是关于 server 的
1 | /* |
struct sockaddr
第一个是
1 |
|
这里使用了前三个 var,
1 | struct sockaddr_in myaddr; /* struct variable to store the address of server */ |
第一个 AF_INET
是明确协议,AF_INET
(又称 PF_INET
)是 IPv4
网络协议的套接字类型,AF_INET6
则是 IPv6
的;而 AF_UNIX 则是 Unix 系统本地通信。
第二个明确地址,因为 sockaddr_in 是双层的引用,所以这边也要引用两次……为什么呢?
inet_addr则是下一个
1 |
|
第三个是端口设置
1 | myaddr.sin_port = htons(port_num); |
至于为什么要转换order呢? 网络字节是大端字节顺序,如果机器是小端字节,那就要 change
CSAPP 说过有小尾端和大尾端
在Linux和Windows网络编程时需要用到htons和htonl函数,用来将主机字节顺序转换为网络字节顺序。
在Intel机器下,执行以下程序
int main()
{
printf(“%d /n”,htons(16));
return 0;
}
得到的结果是4096,初一看感觉很怪。
解释如下,数字16的16进制表示为0x0010,数字4096的16进制表示为0x1000。 由于Intel机器是小尾端,存储数字16时实际顺序为1000,存储4096时实际顺序为0010。因此在发送网络包时为了报文中数据为0010,需要经过htons进行字节转换。如果用IBM等大尾端机器,则没有这种字节顺序转换,但为了程序的可移植性,也最好用这个函数。
另外用注意,数字所占位数小于或等于一个字节(8 bits)时,不要用htons转换。这是因为对于主机来说,大小尾端的最小单位为字节(byte)。
不记得大小尾看这里 https://blog.csdn.net/chy555chy/article/details/51966160 ,简单来说就是数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址,还是相反,只有在 byte 下面会工作哦~
socket(AF_INET, SOCK_DGRAM, 0)
这个的代码是
1 | if ((servSocket = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { |
原型是
1 | int socket(int domain, int type, int protocol); |
domain: 通讯标准(大概吧),之前是AF_INET所以就继续
type: specifies the communication semantics,通讯语义?TCP 使用 sock_dtream,UDP 使用 sock_dgram 居多,一个可靠双工,一个就送一次
TCP (SOCK_STREAM
) is a connection-based protocol. The connection is established and the two parties have a conversation until the connection is terminated by one of the parties or by a network error.
两边维持通信,直到一方关闭或者遇到错误
UDP (SOCK_DGRAM
) is a datagram-based protocol. You send one datagram and get one reply and then the connection terminates.送一次,得到回复,关闭
protocol:制定应该使用哪一个协议,通常来说一个 domain 只有一个协议,所以设置为 0 就可以
它会返回一个文件描述符,如果成功打开的话是个非负整数,具体请看:https://blog.csdn.net/qq_26222859/article/details/52068615
跟stream不一样,它是整数而不是指针(当真)?这也是使用文件最常用的方式
bind(servSocket, (struct sockaddr *)&myaddr, sizeof(myaddr)
1 |
|
嗯,下面这个是使用
1 | if (bind(servSocket, (struct sockaddr *)&myaddr, sizeof(myaddr)) < 0) { |
When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd. addrlen specifies the size, in bytes, of the address structure pointed to by addr.
当socket创造的时候,name space(坑)有了,但是要用 bind 去赋值,第一个是socket返回的文件描述符,然后是myaddr的地址,强行转换成指向struct sockaddr的指针,最后是它的大小。返回的还是文件描述符,所以
下一个是收听端口了
recvlen = recvfrom(servSocket, buf, BUFSIZE, 0, (struct sockaddr *)&remaddr, &addrlen);
原型函数是
1 | ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, |
最后一个
1 | if (sendto(servSocket, buf, strlen(buf), 0, (struct sockaddr *)&remaddr, sizeof(remaddr)) < 0){ |
很好理解了,fd在小于0就是错误,剩下的参数和上面的recvfrom一样的
TCP 连接
listen(servSocket,0);
前面是 serv,后面是pending的连接数,这个只能有一个连接,不接受等待
conn_sock=accept(servSocket,NULL,NULL);
把等待queue的第一个提取出来
address
Either a null pointer, or a pointer to a sockaddr structure where the address of the connecting socket shall be returned.
不太明白什么意思……下次问问
然后会创建一个新的socket连接处理这个,conn_sock 就是新的socket
read,成功之后upper case,然后write回去,结束
总之,从client和server一起写一下吧
client server 共同
创建 struct sockaddr_in ,填入 detail
server端 bind,创建监听端口?不对,connect和bind应该有类似之处?
TCP 和 UDP 都一样,myaddr创造struct, serve socket 是 file discriptor,两个分别赋值,然后 client 用 connect 结合,server 用 bind 结合。
connect 搞定三次握手,用新的 conn_sock,不同于listen的端口传递数据。write 发送,read收到,close四次挥手。
server的话先是listen创建监听端口,然后收到客户再搞出新的专属socket conn_sock,用read读取,write写回
read write 都是
extern ssize_t read(int __fd, void *__buf, size_t __nbytes)
Read NBYTES into BUF from FD. Return the
number read, -1 for errors or 0 for EOF.
从 fd 端口读取 nbytes 到 buf,write 也是,client是 client socket,server 是 conn_socket,果然是万物皆文件啊……感叹.jpg
多线程
首先需要一个 func 来规定这个 thread 要执行什么代码
1 | void *thread_code(void *arg) { |
这个 func 必须返回一个指向 void 的指针,参数值一个指向 void 的指针(我相信是 thread 的运行地址?)
void *thread_code 代表的是返回值是指针,void (*thread_code) 才是,thread_code是指向返回值是 void 的函数的指针
1 | int pthread_create(pthread_t *thread, const pthread_attr_t *attr, |
给出的例子是
1 | pthread_t threads[10]; |
第一个是获得这个 thread 的 id 的地址,第二个先不管,第三个是指向 指向 func 指针 的指针,第四个是传入的参数,是 void 指针类型,需要强制转化
&threads[1] 是 addr of second element, where second element st
为了避免成为孤儿进程,需要combine到主进程里面
1 | int pthread_join(pthread_t thread, void **retval); |
例子里的答案是这么用的
1 | for (i = 0; i < argc-1; ++i) { |
第一个参数是 thread 的 id(一个数组),然后下一个是储存返回值,一个指向指针的指针,跟 **argv 一致,可以存储传回的多个 val
执行是 gcc -lpthread threaded_code.c
跟 lab 不一样
*划重点,void 是通用指针类型,在转换到各种类型指针和转化回来的时候不会丢失数据
Mutex lock
直接看代码吧
1 |
|
https://stackoverflow.com/questions/34524/what-is-a-mutex
这个解释的比较清楚,是允许某个进程说话的橡皮鸭子(
临界区内部,通过互斥锁(mutex)保证只有一个线程可以访问,因此临界区内的变量不需要是volatile的;而在临界区外部,被多个线程访问的变量应为volatile,这也符合了volatile的原意:防止编译器缓存(cache)了被多个线程并发用到的变量。
嗯嗯这是 volatile 的使用
1 | pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; |
这两个语句是等效的
多线程 server
这个主要改得是 server
1 | void *memset(void *str, int c, size_t n) |
- str − This is a pointer to the block of memory to fill.
- c − This is the value to be set. The value is passed as an int, but the function fills the block of memory using the unsigned char conversion of this value.
- n − This is the number of bytes to be set to the value.
大概是 memory set, 将 void 这块block 用 c 的值填满,c 将会变成 unsigned char,然后一共有 n 个
1 | listen(servSocket,BACKLOG); |
一共可以排队等待 backlog 个client
我去这个是真的猛,还要包括 head 文件使用
ref:
https://www.gta.ufrj.br/ensino/eel878/sockets/sockaddr_inman.html
https://zhidao.baidu.com/question/127548969.html
https://pubs.opengroup.org/onlinepubs/009695399/functions/inet_addr.html
https://blog.csdn.net/seven407/article/details/6096702
https://man7.org/linux/man-pages/man2/socket.2.html
https://stackoverflow.com/questions/5815675/what-is-sock-dgram-and-sock-stream
https://man7.org/linux/man-pages/man2/bind.2.html
https://www.ibm.com/docs/en/zos/2.1.0?topic=functions-recvfrom-receive-messages-socket